内排序就是在内存里排序。外排序涉及内、外存间的数据交换。
一 交换排序
1 冒泡排序
- 时间复杂度O(n^2)
- 辅助空间复杂度O(1)(就地排序)
- 稳定(相等的元素排序前后相对位置不变)
- 每次排序有元素归位(homing)
void BubbleSort(RecType R[], int n)
{
for(int i=0;i<n-1;i++)
{
bool exchange=false;//本趟排序有没有元素交换
for(int j=n-1;j>i;j--)//从后往前扫,循环n-i-1次
if(R[j].key<R[j-1].key)
{ swap(Rj],R[j-1]);
exchange=true;//有元素交换
}
if(!exchange)//本趟没有交换就可以终止了
return;
}
}
2 快速排序(所有排序中平均性能最好)
- 时间复杂度O(nlog2n);最坏情况时间复杂度O(n^2)
- 空间复杂度O(log2n);最坏空间复杂度O(n);非就地排序!
- 不稳定(相等的元素排序前后相对位置发生变化)
- 每一趟划分将基准(中轴)归位
//一趟划分
int partition(RecType R[],int s,int t)//对R[s..t]进行划分
{
int i=s,j=t;//i指头,j指尾
RecType tmp=R[i];//基准(中轴)放到tmp里
while(i<j)
{
while(j>i&&R[j].key>=tmp.key)
j--;//从后面开始扫如果比基准大就j--一直找到比基准小的一个R[j]
R[i]=R[j];//把这个比基准小的R[j]移到前面
while(i<j&&R[i].key<=tmp.key)
i++;//从前面开始扫如果比基准小就i++一直找到比基准大的一个R[i]
R[j]=R[i];//把这个比基准大的R[i]移到后面
}
R[i]=tmp;
return i;
}
void QuikSort(RecType R[],int s,int t)//对R[s..t]进行排序
{ int i;//中轴位置
if(s<t)
{ i=partition(R,s,t);
QuikSort(R,s,i-1);//中轴左边递归调用
QuikSort(R,s,i+1);//中轴右边递归调用
}
}
对(10,18,4,3,6,12,1,9,18,8)做一趟快速排序:
基准为tmp=R[0]=10,partition:
R0 | R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | i | j | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
10 | 18 | 4 | 3 | 6 | 12 | 1 | 9 | 18 | 8 | 0 | 9 | i<j | |
8 | 18 | 4 | 3 | 6 | 12 | 1 | 9 | 18 | 18 | 1 | 9 | i<j | |
8 | 9 | 4 | 3 | 6 | 12 | 1 | 12 | 18 | 18 | 5 | 7 | i<j | |
8 | 9 | 4 | 3 | 6 | 1 | 1 | 12 | 18 | 18 | 6 | 6 | ||
R[i]=tmp | 8 | 9 | 4 | 3 | 6 | 1 | 10 | 12 | 18 | 18 | 6 | 6 |
返回i=6
二 插入排序
每趟排序不产生全局有序区,就地排。
1 直接插入排序&折半插入排序
- 平均时间复杂度O(n^2)
- 空间复杂度O(1)
- 稳定
- 每趟不产生全局有序区(不一定归位)
每趟排序将无序区的第一个元素插入到有序区(增量法)
void InsertSort(RecType R[],int n)//对R[0..n-1]递增排序
{ int i;//i表示第i趟排序; 排了几趟有序区就有几个元素
int j;//j用来扫描有序区(R[0]...R[i-1]);
RecType tmp;//tmp里放无序区第一个元素R[i]
for(i=1;i<n;i++)
{ //有序区递增,最后一个元素最大;
//无序区第一个元素如果比有序区最后一个大,直接i++进入下一轮循环;
//无序区第一个元素比有序区最后一个小,插入有序区
if(R[i].key<R[i-1].key)
{ tmp=R[i];
j=i-1;//从后往前扫有序区
//有序区内key大于tmp.key的都后移一个位置
do{ //首先要把R[i-1](这一轮循环的有序区最后一个元素)挪到R[i]上
R[j+1]=R[j];
j--;
}while (j>=0&&R[j].key>tmp.key);//凡是比tmp.key小的都后挪一个位置
R[j+1]=tmp;//在j+1处插入R[i]
}
}
}
直接插入查找位置时是顺序比较。
将无序区开头元素R[i]插入有序区时使用折半查找方法,可减少关键字比较次数。
折半插入排序移动元素的性能没有改善。
时间复杂度O(n^2);空间复杂度O(1);稳定。
void BinInsertSort(RecType R[],int n)
{ int i,j;
int low,high,mid;
RecType tmp;
for(i=1;i<n;i++)
{ if(R[i].key<R[i-1].key)
{ tmp=R[i];
//折半查找插入位置
low=0;high=i-1;
while(low<high)
{ mid=(low+high)/2;
if(tmp.key<R[mid].key)
high=mid-1;
else
low=mid+1;
}//最后high=low=mid
//集中进行元素后移
for(j=i-1;j>=high+1;j--)
R[j+1]-R[j];
//插入
R[high+1]=tmp;
}
}
}
2 希尔排序(Shell Sort)
- 时间复杂度O(n^1.3)
- 空间复杂度O(1)
- 不稳定
- 每一趟不产生有序区
分组插入。
增量d:R[i+n*d] n=(0,1,2…) 是一组。
对每组直接插入排序。
希尔排序是减少增量的排序方法。
void ShellSort(RecType R[],int n)
{ int i,j,d;
RecType tmp;
d=n/2; //增量置初值
while(d>0)
{ for(i=d;i<n;i++) //扫描所有组
{ tmp=R[i]; //对相隔d位置的一组采用直接插入排序
j=i-d;
while(j>=0&&tmp.key<R[j].key)
{ R[j+d]=R[j];
j=j-d;
}
R[j+d]=tmp;
}
d=d/2; //减小增量
}
}
三 选择排序
每趟产生全局有序区!(很多元素找最小的几个就很合适)
就地排!
1 直接选择排序
- 时间复杂度O(n^2)
- 空间复杂度O(1)
- 不稳定
- 每趟都产生全局有序区(有元素归位)
将无序区最小的元素选出来放在无序区第一个。
void SelectSort(RecType R[],int n)
{ int i,j;
for(i=0;i<n-1;i++)
{ int k=i;//k存放R[i..n-1]中最小关键字的下标,初值为i
for(j=i+1;j<n;j++)//扫描无序区所有元素
if(R[j].key<R[j+1].key)
k=j;
if(k!=i)
swap(R[i],R[k]);
}
}
2 堆排序
- 最好最坏时间复杂度O(nlog2n)(时间性能与初始序列无关)
- 空间复杂度O(1)
- 不稳定
- 每趟都产生全局有序区(有元素归位)
是一种树形选择排序方法。完全二叉树使用顺序存储结构。堆排序将数组看成完全二叉树的顺序存储结构进而排序。
堆的定义:R[1…n]是含n个关键字的序列,满足
爸爸结点总是大于孩子结点(大根堆) 或
爸爸结点总是小于孩子结点(小根堆)。
R[1…n]从1开始是因为把每个元素当成结点,序号是指第几个结点。
对于有n个结点的完全二叉树,最后一个分支结点是第(n/2)个结点。
即对于R[1…n/2]: R[i]<=(>=)R[2i]&&R[i]<=(>=)R[2i+1]。
构建初始堆:从最后一个分支结点开始筛选堆。
筛选堆:挑出 某分支结点的子树 里 最大的元素 放在这个分支节点上。
//筛选
void sift(RecType R[],int low,int high)//从某分支结点R[low]开始筛选
{ int i=low,j=2*i; //i指向当前的分支结点;j指向孩子里大的那个
RecType tmp=R[i]; //tmp存放筛选入口的那个分支结点
while(j<=high)
{ if(j<high&&R[j].key<R[j+1].key)//右孩子较大则j指向右孩子
j++;
if(tmp.key<R[j].key)//tmp(筛选入口分支结点的值)和当前j指向的孩子比较
{ R[i]=R[j]; //大孩子换到原分支结点上
i=j; j=2*i; //大孩子成为新的分支结点
}
else break;
}
R[i]=tmp;//将tmp放到i指向的地方
}
//堆排序
void HeapSort(RecType R[],int n)
{
//初始化堆
for(int i=n/2;i>=1;i--)
sift(R,i,n);
//进行n-1趟完成堆排序
for(int i=n;i>=2;i--)
{ swap(R[1],R[i]);//R[1]总是最大的,swap以后归位了。
sift(R,1,i-1);//对swap上来的新根结点进行重新筛选
//小根堆的话从小到大排序直接sift(R,2,i)
}
}
对(80,70,33,65,24,56,48)初始化堆(小根堆):
黄色为tmp(筛选入口那个分支结点),从n/2开始
·4· | ·5· | ‘6’ | ‘7’ | ||||
---|---|---|---|---|---|---|---|
80 | 70 | 33 | 65 | 24 | 56 | 48 | i=3,j=7 |
80 | 70 | 33 | 65 | 24 | 56 | 48 | i=2,j=5; i=5,j=10; |
80 | 24 | 33 | 65 | 70 | 56 | 48 | i=1,j=2; i=2,j=4; i=4,j=8; |
24 | 65 | 33 | 80 | 70 | 56 | 48 | 结果 |
四 归并排序
- 对长度为n的排序表,二路归并需要进行(log2n)趟,每趟时间为O(n)。所以最好最坏时间复杂度都为O(nlog2n)。
- 空间复杂度O(n);非就地排序!
- 稳定
- 每趟产生局部有序区 不一定归位
将两个有序表合并成新有序表:两个有序表在一个数组中相邻放置,合并到局部暂存数组R1,最后将R1复制回R。
//表1:R[low..mid] 表2:R[mid+1..high]
void Merge(RecType R[],int low,int mid, int high)
{ RecType *R1;
int i=low,j=mid+1;
int k=0; //k是R1的下标
R1=(RecType*)malloc((high-low+1)*sizeof(RecType));
//二路归并部分
while(i<=mid&&j<=high)
if(R[i].key<=R[j].key)
{R1[k]=R[i];i++;k++;}
else
{R1[k]=R[j];j++;k++;}
//表1或表2有剩的
while(i<=mid)
{R1[k]=R[i];i++;k++}
while(j<=high)
{R1[k]=R[j];j++;k++}
//R1复制回R
for(k=0,i=low;i<=high;k++,i++)
R[i]=R1[k];
free(R1);
}
二路归并排序:
从length=1开始二路归并,每次length增大2倍,一共进行(log2n)趟
void MergeSort(RecType R[],int n)
{ int length;
for(length=1;length<n;length=length*2)
MergePass(R,length,n);
}
//length为每个有序表的长度,n为子表个数(不是下标)
void MergePass(RecType R[],int length,int n)
{ int i;
//两个两个归并
for(i=0;i+2*length-1<n;i=i+2*length)
Merge(R,i,i+length-1,i+2*length-1);
//只有偶数个子表时才可能满足下面的if条件
//有奇数个子表的话不满足if条件,最后那个子表不处理
if(i+length-1<n-1)
Merge(R,i,i+length-1,n-1);
}