内部排序
1.概述
一、什么是排序?
排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。
一般情况下,假设含n个记录的序列为{R1,R2,…,Rn},其相应的关键字序列为 { K1, K2, …,Kn },这些关键字相互之间可以进行比较,即在它们之间存在着这样一个关系Kp1≤Kp2≤…≤Kpn,按此固有关系将上式记录序列重新排列为{ Rp1, Rp2, …,Rpn },的操作称作排序。
假设对记录的次关键字进行排序,记录之中有两条记录Ri 和Rj,它们的关键字Ki和Kj相等,在排序之前记录Ri 在Rj之前,若排序之后记录Ri 仍在Rj之前,则称排序方法是稳定的;相反,若经过某种排序之后Ri在Rj在之后,则称所用的排序方法是不稳定的。
二、内部排序和外部排序
若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序;反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
书写规范:数组第一个元素L[0]预留,数组长度为L.length-1,尽可能采用和课本相同命名
2.插入排序
插入排序是一种简单直观的排序方法,其基本思想是每次将一个待排序的记录按其关键字大小插入到前面己排好序的子序列中,直到全部记录插入完成。由插入排序的思想可以引申出三个重要的排序算法: 直接插入排序、折半插入排序和希尔排序。
2.1 直接插入排序
static void InsertSort(int[] L){//对数组L作直接插入排序.
for (int i = 2; i <=L.length-1 ; i++) {//从第二个元素到最后一个元素,依次往前插入
if(L[i]<L[i-1]){//要插入的元素比前一个小,如果大于等于就不动
L[0]=L[i];//将L[i]暂存到L[0]
L[i]=L[i-1];//前一个元素往后移
int j;
for ( j= i-2; L[0]<L[j]; j--) {//L[0]比前一个小,继续元素往后移
L[j+1]=L[j];
}
L[j+1]=L[0];
}
}
}
2.2-1 折半插入排序
相比直接插入,减少比较次数
static void BinsertSort(int[] L){//对颉序表L作折半插入排序
for (int i = 2; i <=L.length-1 ; i++) {
L[0]=L[i];//将L[i]暂存到L[O]
int low=1,high=i-1;
while(low<=high){
int m=(low+high)/2;
if(L[0]<L[m]){
high=m-1;
}else{
low=m+1;
}
}
for(int j=i-1;j>=high+1;j--){//最终状态一定是high在左,low在右,low的位置是插入位置
L[j+1]=L[j];
}
L[high+1]=L[0];
// for(int j=i-1;j>=low;j--){//上面四行可以替换一下
// L[j+1]=L[j];
// }
// L[low]=L[0];
}
}
2.2-2 2路插入排序
相对上面减少移动
此代码由于涉及到+n%n,第一个元素L[0]不预留
static void TWayInsertSort(int[] L){
int[] TP=new int[L.length];
int n=L.length;
TP[0] = L[0];
int head, tail;
head = tail = 0;//两个都指向第一个元素
for(int i=1; i<n; ++i) {
if(L[i] < TP[head]) {//待排序元素小于头往前插
head = (head-1+n)%n;
TP[head] = L[i];
}
else if(L[i] > TP[tail]) {//待排序元素大于头往后插
tail++;
TP[tail] = L[i];
}else{//往中间插
tail++;
TP[tail] = TP[tail-1];//把最后一个元素往后移动1位
int j;
for(j=tail-1;L[i]<TP[(j-1+n)%n]; j=(j-1+n)%n) {//L[i]如果小于一开始的倒数第二个元素TP[(j-1+n)%n],继续移动
TP[j] = TP[(j-1+n)%n];//将元素往后移动一个
}
TP[j] = L[i];
}
}
for(int i=0; i<n; ++i) {
L[i] = TP[head];
head = (head+1)%n;
}
}
2.2-3 表插入排序
表插入排序,即使用链表的存储结构对数据进行插入排序。在对记录按照其关键字进行排序的过程中,不需要移动记录的存储位置,只需要更改结点间指针的指向。
在使用数组结构表示的链表中,设定数组下标为 0 的结点作为链表的表头结点,并令其关键字取最大整数。则表插入排序的具体实现过程是:首先将链表中数组下标为 1 的结点和表头结点构成一个循环链表,然后将后序的所有结点按照其存储的关键字的大小,依次插入到循环链表中。
例如,将无序表{49,38,76,13,27}
用表插入排序的方式进行排序,其过程为:
- 首先使存储 49 的结点与表头结点构成一个初始的循环链表,完成对链表的初始化,如下表所示:
- 然后将以 38 为关键字的记录插入到循环链表中(只需要更改其链表的 next 指针即可),插入后的链表为:
- 再将以 76 为关键字的结点插入到循环链表中,插入后的链表为:
- 再将以 13 为关键字的结点插入到循环链表中,插入后的链表为:
- 最后将以 27 为关键字的结点插入到循环链表中,插入后的链表为:
- 最终形成的循环链表为:
从表插入排序的实现过程上分析,与直接插入排序相比只是避免了移动记录的过程(修改各记录结点中的指针域即可),而插入过程中同其它关键字的比较次数并没有改变,所以表插入排序算法的时间复杂度仍是O(n2)。
对链表进行再加工
在表插入排序算法求得的有序表是用链表表示的,也就注定其只能进行顺序查找。而如果想用折半查找的算法,就需要对链表进行再加工,即对链表中的记录进行重新排列,具体做法为:遍历链表,将链表中第 i 个结点移动至数组的第 i 个下标位置中。
例如,上表是已经构建好的链表,对其进行再加工的过程为:
- 首先,通过其表头结点得知记录中关键字最小的是数组下标为 4 的关键字 13,而 13 应该放在数组下标为 1 的位置,所以需要同下标为 1 中存放的关键字进行调换。但是为了后期能够找到 49,将 13 的 next 域指向 49 所在的位置(改变之前需要保存原来的值,这里用 q 指针表示),如下表所示:
- 然后通过 q 指针找到原本 13 指向的下一位关键字 27,同时 q 指针指向下标为 2 的关键字 38,由于 27 应该移至下标为 2 的位置,所以同关键字 38 交换,同时改变关键字 27 的 next 域,如下表所示:
- 之后再通过 q 指针找到下一位关键字时,发现所指位置为下标 2,而之前已经经过了 2 次 移动,所以可以判定此时数组中存放的已经不是要找的,所以需要通过下标为 2 中的 next 域继续寻找,找到下标为 5 的位置,即关键字 38,由于下标 5 远远大于 2,可以判断 38 即为要找的值,所以同下标为 3 的记录交换位置,还要更改其 next 域,同时将 q 指针指向下标为 1 的位置,如下表所示:
- 然后通过 q 指针找到下一位关键字,由于其指向位置的下标 1 中的记录已经发生移动,所以通过 next 域找到关键字 49,发现它的位置不用改变;同样,当通过关键字 49 的 next 域找到下标为 3 的位置,还是需要通过其 next 域找到关键字 76 ,它的位置也不用改变。
static class SLinkListType{
int data;
int next;
}
static void TableInsertSort(SLinkListType[] t,int n){
t[0].next=1;
int p,q;
for(int i=2;i<=n;i++){
p=t[0].next;
q=0;
while (p!=0&&t[p].data<=t[i].data){
q=p;
p=t[p].next;
}
t[i].next=t[q].next;
t[q].next=i;
}
}
static void Arrange(SLinkListType[] SL){
//令 p 指向当前要排列的记录
int p=SL[0].next;
for (int i=1; i<SL.length-1; i++) {
//如果条件成立,证明原来的数据已经移动,需要通过不断找 next 域,找到其真正的位置
while (p<i) {
p=SL[p].next;
}
//找到之后,令 q 指针指向其链表的下一个记录所在的位置
int q=SL[p].next;
//条件成立,证明需要同下标为 i 的记录进行位置交换
if (p!=i) {
SLinkListType t=new SLinkListType();
t=SL[p];
SL[p]=SL[i];
SL[i]=t;
//交换完成后,该变 next 的值,便于后期遍历
SL[i].next=p;
}
//最后令 p 指向下一条记录
p=q;
}
}
static void TableInsertSort(int[] a){
SLinkListType[] t=new SLinkListType[a.length];
for (int i = 0; i <t.length ; i++) {
t[i]=new SLinkListType();
t[i].next=0;
t[i].data=a[i];
}
t[0].data=Integer.MAX_VALUE;
TableInsertSort(t,t.length-1);
Arrange(t);
for (int i = 0; i < t.length; i++) {
a[i]=t[i].data;
}
a[0]=0;
}
2.3 希尔排序
static void ShellInsert(int[] L,int dk){
//对顺序表L作一趟希尔插人排序。本算法是和一趟直接插入排序相比,作了以下修改:
//1.前后记录位置的增量是dk,而不是1;
//2. r[0]只是暂存单元,不是哨兵。当j<=0时,插人位置已找到。
for (int i=dk+ 1; i<= L.length-1; i++ ) {
if (L[i]<L[i-dk]) { // 如果L[i]比前dk小,则插入
L[0]=L[i];//暂存在L[0]
int j;
for(j=i-dk; j>0 && L[0]<L[j]; j-=dk) {//j初始为前一个元素
L[j + dk] = L[j];//j移到i的位置
}
L[j + dk] = L[0];//插入
}
}
}
static void ShellSort(int[] a){
//对应原书dlta[n]={(a.length-1)/2,(a.length-1)/4,(a.length-1)/8,......1}
for(int interval=(a.length-1)/2;interval>0;interval=interval/2){
ShellInsert(a,interval);///一趟增量为interval的插人排序
}
}
3.快速排序
-
分解:数组A[p..r]被划分为两个子数组A[p. .q-1]和A[q+1,r],使得A[q]为大小居中的数,左侧A[p. .q-1]中的每个元素都小于等于它,而右侧A[q+1,r]中的每个元素都大于等于它。其中计算下标q也是划分过程的一部分。
-
解决:通过递归调用快速排序,对子数组A[p. .q-1]和A[q+1,r]进行排序
-
合并:因为子数组都是原址排序的,所以不需要合并,数组A[p. .r]已经有序
那么,划分就是问题的关键
快排的划分算法
1.一遍单向扫描法:
- 一遍扫描法的思路是,用两个指针将数组划分为三个区间
- 扫描指针(scan_pos)左边是确认小于等于主元的
- 扫描指针到某个指针(next_bigger_pos)中间为未知的,因此我们将第二个指针(next_bigger_pos) 称为未知区间末指针,末指针的右边区间为确认大于主元的元素
static int partition(int a[],int p,int q){
int privot=p;
int sp=p+1;
int bigger=q;
while (sp<=bigger){
if(a[sp]<=a[p]){
sp++;
}else{
swap(a,sp,bigger);
bigger--;
}
}
swap(a,privot,bigger);
return bigger;
}
2.双向扫描法
双向扫描的思路是,头尾指针往中间扫3描,从左找到大于主元的元素,从右找到小于等于主元的元素二者交换,继续扫描,直到左侧无大元素,右侧无小元素
static int partition2(int a[],int p,int q){
int privot=p;
int left=p+1;
int right=q;
while(left<=right){
while(left<=right&&a[left]<=a[privot]) left++;
while(left<=right&&a[right]>a[privot]) right--;
if(left<right){
swap(a,right,left);
}
}
swap(a,privot,right);
return right;
}
3.严蔚敏教材交换法
附设两个指针low和high,它们的初值分别为low和high,设枢轴记录的关键字为pivotkey,则首先从high所指位置起向前搜索找到第一个关键字小于pivotkey的记录和枢轴记录互相交换,然后从low所指位置起向后搜索,找到第一个关键字大于pivotkey的记录和枢轴记录互相交换,重复这两步直至low=high为止。
static int partition3(int r[],int low,int high){
int pivot=r[low];//用子表的第一个记录作枢轴记录
while(low<high){//从表的两端交替地向中间扫描
//如果是第一个元素做枢轴,则先从high往前扫描,最后一个,则先从low往后扫描
while (low<high&&r[high]>=pivot){
high--;
}
swap(r,low,high);//将比枢轴记录小的记录交换到低端
while(low<high&&r[low]<=pivot){
low++;
}
swap(r,low,high);//将比枢轴记录大的记录交换到高端
}
return low;//返回枢轴所在位置
}
4. 严蔚敏教材赋值法
具体实现上述算法时,每交换一对记录需进行3次记录移动(赋值)的操作。而实际上,在排序过程中对枢轴记录的赋值是多余的,因为只有在一趟排序结束时,即low=high的位置才是枢轴记录的最后位置。由此可改写上述算法,先将枢轴记录暂存在r[0]的位置上,排序过程中只作r[Iow]或r[high]的单向移动,直至一趟排序结束后再将枢轴记录移至正确位置上。
static int partition4(int r[],int low,int high){
int pivot=r[low];//因为r[low]后面要覆盖,先把他储存在pivot中
while(low<high){
while (low<high&&r[high]>=pivot){
high--;
}
r[low]=r[high];
while(low<high&&r[low]<=pivot){
low++;
}
r[high]=r[low];
}
r[low]=pivot;
return low;
}
函数调用
static void QSort (int[] L,int low,int high){//对顺序表L中的子序列L[1ow..high]作快速排序
if (low< high) {//长度大于1
int pivotloc = partition4(L, low, high);//将L[1ow. . high]一分为二
QSort(L, low, pivotloc- 1);//对低子表递归排序,pivotloc是枢轴位置
QSort(L,pivotloc+ 1, high) ;//对高子表递归排序
}
}
static void QuickSort(int[] L) {//对顺序表L作快速排序。
QSort(L, 1,L.length-1) ;
}
工程实践中的其他优化
- 三点中值法
- 绝对中值法
- 待排序列表较短时,用插入排序
4.选择排序
4.1 简单选择排序
static int SelectMinKey(int[] L,int i){
int min=i;
for (int j = i; j <=L.length-1; j++) {
if(L[j]<L[min]){
min=j;
}
}
return min;
}
static void SelectSort (int[] L) {//对顺序表L作简单选择排序,只需要从第一个到倒数第二个排序即可
for (int i = 1; i < L.length - 1; ++i) {//选择第i小的记录,并交换到位
int j = SelectMinKey(L, i);//在L[i..L.length-1]中选择key最小的记录
if (i != j) swap(L, i, j);//与第i个记录交换
}
}
4.3 堆排序
使记录序列按关键字非递减有序排列,则在堆排序的算法中先建一个“大顶堆”,即先选得-个关键字为最大的记录并与序列中最后一个记录交换,然后对序列中前n-1记录进行筛选,重新将它调整为一一个“大顶堆”,如此反复直至排序结束。由此,“筛选"应沿关键字较大的孩子结点向下进行。
static void HeapAdjust(int[] H,int s,int m){
// 已知s.. m中记录的关键字除H[s]之外均满足堆的定义,本函数调整H[s]的关键字,使H[s..m]成为一个大顶堆
int rc=H[s];//先把父亲节点值存到rc中
for (int j = 2*s; j <=m ; j*=2) {//j为s的左孩子,循环一次后,j*2,变成最大孩子的左孩子
if(j<m&&H[j]<H[j+1]){//如果右孩子大于左孩子,j++,j就为最大孩子的坐标
j++;
}
if(rc>=H[j]){//父元素已经是两个孩子中最大的了,则for循环退出
break;
}
H[s]=H[j];//把最大孩子的值赋值给父亲节点
s=j;//s保存当前最大孩子的下标
}
H[s]=rc;//把父亲值赋值给最后一次孩子下标
}
//递归调整
static void HeapAdjust2(int A[],int i,int n){//i为父节点,本函数使A[i..n]成为大顶堆
int left=2*i;
int right=2*i+1;
if(left>n){//左孩子已经越界返回
return;
}
int max=left;
if(right<=n&&A[right]>A[left]){
max=right;
}//max指向左右孩子中较大的那个
if(A[i]>=A[max]){//如果A[i]把;两个孩子都大,不同调整
return;
}
_快速排序.swap(A,max,i); //否则,找到两个孩子中较大的,和i交换
HeapAdjust2(A,max,n); //大孩子那个位置的值发生了变化,i变更为大孩子那个位置,递归调整
}
static void HeapSort(int[] H){
//H.length-1相当于H的length
for(int i=(H.length-1)/2;i>0;i--){//把H.r[1.. H.length]建成大顶堆
HeapAdjust2(H,i,H.length-1);//从(H.length-1)/2位最后一个父亲节点,依次往前调整
}
for (int i=H.length-1; i >1 ; i--) {
swap(H,1,i);//将堆顶(1)记录和当前未经排序子序列H[1.. i]中最后一个记录相互交换
HeapAdjust2(H,1,i-1);//将H[1..i-1]重新调整为大顶堆
}
}
5.归并排序
- 归并排序(Merge Sort)算法完全依照了分治模式
- 分解:将n个元素分成各含n/2个元素的子序列;
- 解决:对两个子序列递归地排序;
- 合并:合并两个已排序的子序列以得到排序结果
- 和快排不同的是
- 归并的分解较为随意
- 重点是合并
static void Merge(int[] SR,int[] TR,int i,int m,int n){
//将SR左右半有序。归并到TR
int k,j;//k为TR合并位置下标,i为左边最小元素下标,j为右边最小元素下标
for(k=i,j=m+1;i<=m&&j<=n;++k){
if(SR[i]<SR[j]){
TR[k]=SR[i++];
}else{
TR[k]=SR[j++];
}
}
while(i<=m){
TR[k++]=SR[i++];
}
while (j<=n){
TR[k++]=SR[j++];
}
}
static void MSort(int[] SR,int[] TR1,int s,int t){
//SR为拷贝,TR1为要排序数组
int TR2[]=new int[TR1.length];//TR2为拷贝的拷贝
if(s==t) {
TR1[s] = SR[s];
}else{
int m=(s+t)/2;//将SR[s..t]平分为SR[s..m]和SR[m+ 1..t]
MSort(SR,TR2,s,m);//递归地将SR[s..m]归并为有序的TR2[s..]
MSort(SR,TR2,m+1,t);// 递归地将SR[m+ 1.. t]归并为有序的TR2[m+ 1..t]
Merge(TR2,TR1,s,m,t);//将TR2[s.. m]和TR2[m+ 1.. t]归并到TR1[s..t]
}
}
static void MergeSort(int[] a){
int[] b=a.clone();//b为拷贝,a为原数组
MSort(b,a,1,a.length-1);
}
6.基数排序
基数排序(Radix Sorting) 是和前面所述各类排序方法完全不相同的一种排序方法。从前几节的讨论可见,实现排序主要是通过关键字间的比较和移动记录这两种操作,而实现基数排序不需要进行记录关键字间的比较。基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。
6.1 多关键字的排序
n 个记录的序列 { R1, R2, …,Rn}对关键字 (Ki0, Ki1,…,Kid-1) 有序是指: 对于序列中任意两个记录 Ri 和 Rj(1≤i<j≤n) 都满足下列(词典)有序关系: (Ki0, Ki1, …,Kid-1) < (Kj0, Kj1, …,Kjd-1)其中: K0 被称为 “最主”位关键字,Kd-1 被称为 “最次”位关键字
6. 2 链式基数排序
思路:是一种特殊的桶排序
初始化0-9号十个桶
一、按个位数字,将关键字入桶,入完后,依次遍历10个桶,按检出顺序回填到数组中,如
321 322 331 500 423 476 926
0 | 500 |
---|---|
1 | 321 331 |
2 | 322 |
3 | 423 |
4 | 无 |
5 | 无 |
6 | 476 926 |
7 | 无 |
8 | 无 |
9 | 无 |
检出后数组序列为: 500 321 331 423 476 926,然后取十位数字重复过程一,得到
0 | 500 |
---|---|
1 | 无 |
2 | 321 423 926 |
3 | 331 |
4 | 无 |
5 | 无 |
6 | 476 |
7 | 无 |
8 | 无 |
9 | 无 |
检出后数组序列为: 500 321 423 926 331 476,然后取百位数字重复过程一,得到
0 | 无 |
---|---|
1 | 无 |
2 | 无 |
3 | 321 331 |
4 | 423 476 |
5 | 500 |
6 | 926 |
7 | |
8 | |
9 |
检出后数组序列为: 321 331 423 476 500 926,已然有序
但是我们应该继续入桶,不过因为再高位全部是0了,这些元素会按顺序全部进入0号桶,这时0号桶的size==数组的size,这时结束标志
最后再回填到数组,数组就是升序排列的了
时间复杂度: 假设最大的数有k位,就要进行k次入桶和回填,每次入桶和回填是线性的,所以整体复杂度为kN,
其中k为最大数的10进制位数
空间复杂度:桶是10个,10个桶里面存n个元素,这些空间都是额外开辟的,所以额外的空间是N+k,k是进制
肯定是非原址的了
稳定性:假设有相等的元素,它们会次第入桶,次第回数组,不会交叉,所以是稳定的
static void Distribute(int[] a,int i,ArrayList[] bucket) {
for (int j = 0; j <a.length ; j++) {
int temp =a[j] / (int) (Math.pow(10, i )) % 10;//取出a[j]的个位
bucket[temp].add(a[j]);//加入到相应的bucket
}
}
static void Collect(ArrayList[] bucket,int[] a){
int k = 0;
for (int i = 0; i <bucket.length; i++) {//每个桶中的元素依次压入原数组
for (Object m : bucket[i]) {
a[k++] = (Integer) m;
}
}
//记得清空
for (ArrayList b : bucket) {
b.clear();
}
}
static void RadixSort(int[] a){
int max=a[0];
for (int e:a) {
if(e>max){
max=e;
}
}
int dNum=1;//dNum为最大数的位数
while (max/10!=0){
dNum++;
max=max/10;
}
ArrayList[] bucket=new ArrayList[10];//在Java中只是建立数组,并没有初始化
for (int i = 0; i < bucket.length; i++) {
bucket[i] = new ArrayList();
}
for (int i = 0; i <dNum ; i++) {//按最低位优先依次,从个位数进行分配收集
Distribute(a,i,bucket);//把数组a元素,按个位分配到对用bucket中
Collect(bucket,a);//从bucket[0]-[9]收集到数组a中
}
}
7.各种内部排序方法的比较讨论
一、时间性能
- 平均的时间性能
时间复杂度为 O(nlogn):快速排序、堆排序和归并排序
时间复杂度为 O(n2):直接插入排序、起泡排序和简单选择排序
时间复杂度为 O(n):基数排序: O(d(n+rd))
-
当待排记录序列按关键字顺序有序时
直接插入排序和起泡排序能达到O(n)的时间复杂度;
快速排序的时间性能蜕化为O(n2) -
简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。
二、空间性能
指的是排序过程中所需的辅助空间大小
- 所有的简单排序方法(包括:直接插入、起泡和简单选择) 和堆排序的空间复杂度为O(1);
- 快速排序为O(logn),为递归程序执行过程中,栈所需的辅助空间;
- 归并排序所需辅助空间最多,其空间复杂度为 O(n);
- 链式基数排序需附设队列首尾指针,则空间复杂度为 O(rd)。
测试程序
import java.util.Arrays;
public class _课本排序实现 {
static void swap(int a[],int b,int c){
int t=a[b];
a[b]=a[c];
a[c]=t;
}
static int[] getRandomArr(int length, int min, int max) {//随机生生成长度为length,最小min,最大mx的随机数组
int[] arr = new int[length];
for (int i = 0; i < length; i++) {
arr[i] = (int) (Math.random() * (max + 1 - min) + min);
}
return arr;
}
//此处插入要测试的排序
public static void main(String[] args) {
int[] a= getRandomArr(10,0,10);
a[0]=0;
int[] b=a.clone();
long f=System.currentTimeMillis();
Arrays.sort(b);
System.out.println((System.currentTimeMillis()-f)+"ms 自带");
System.out.println(Arrays.toString(b));
f=System.currentTimeMillis();
SelectSort(a);//此处替换要测试的排序
System.out.println((System.currentTimeMillis()-f)+"ms");
System.out.println(Arrays.toString(a));
boolean isSame=true;
for (int i = 1; i < a.length; i++) {
if(a[i]!=b[i]){
isSame=false;
break;
}
}
System.out.println(isSame);
}
}