插入排序
基本思想:每次将一个待排序的记录按其关键码的大小插入到一个已经排好序的有序序列中,直到全部记录排好序为止。
基本方法:直接插入排序和希尔排序
直接插入排序
基本思想:在插入第 i(i>1)个记录时,前面的 i-1个记录已经排好序。
样例展示:
12 5 9 20 6 31 24
[5 12] 9 20 6 31 24
[5 9 12] 20 6 31 24
[5 9 12 20] 6 31 24
[5 6 9 12 20] 31 24
[5 6 9 12 20 31] 24
[5 6 9 12 20 24 31]
时间复杂度:
最好情况下(正序):
比较次数:n-1
移动次数:2(n-1)
时间复杂度为O(n)。
最坏情况下(逆序或反序):
比较次数:
移动次数:
时间复杂度为O(n^2)。
平均情况下(随机排列):
比较次数:
移动次数:
时间复杂度为O(n^2)。
代码
//插入排序,顺序
void InsertSort(int a[],int n)
{
for(int i=1; i<n; i++)
{
int temp=a[i];
int j=i-1;
while(temp<a[j])
{
a[j+1]=a[j];
j--;
if(j<0) break;
}
a[j+1]=temp;
}
}
希尔排序
基本思想:每一个距离为d的的数取出来作为一个序列进行排序(d1=n/2,di+1=di/2)
快速插入排序相当于d=1
for (d=n/2; d>=1; d=d/2)
{
以d为增量,进行组内直接插入排序;
}
样例展示:
40 25 49 25 16 21 8 30 13
d=4: 13 16 40
21 25
8 49
25 30
13 21 8 25 16 25 49 30 40
d=2:8 21 13 25 16 25 40 30 49
d=1:8 13 16 21 25 25 30 40 49
时间复杂度:
研究表明,希尔排序的时间性能在O(n2)和O(nlog2n)之间。当n在某个特定范围内,希尔排序所需的比较次数和记录的移动次数约为O(n…^1.3 ) 。
代码实现:
//希尔排序,顺序
void ShellSort(int a[],int n)
{
for(int d=n/2;d>=1;d/=2)
{
for(int i=d; i<n; i++)
{
int temp=a[i];
int j=i-d;
while(temp<a[j])
{
a[j+d]=a[j];
j-=d;
if(j<0)
break;
}
a[j+d]=temp;
}
}
}
交换排序
基本思想:在待排序列中选两个记录,将它们的关键码相比较,如果反序(即排列顺序与排序后的次序正好相反),则交换它们的存储位置。
冒泡排序
基本思想:两两比较相邻记录的关键码,如果反序则交换,直到没有反序的记录为止。
样例展示:
12 5 9 20 6 31 24
一趟:[5 9 12 6 20 24] 31
二趟:[5 9 6 ]12 20 24 31
三趟:[5 6] 9 12 20 24 31
四趟:5 6 9 12 20 24 31
时间复杂度:
代码实现:
//冒泡排序
void Bubble(int a[],int n)
{
int change=n-1;
while(change!=-1)
{
int bound=change;
change=-1;
for(int i=0;i<bound;i++)
{
if(a[i]>a[i+1])
{
swap(a[i],a[i+1]);
change=i;
}
}
}
}
快速排序
基本思想:
首先选一个轴值(即比较的基准),
通过一趟排序将待排序记录分割成独立的两部分,
前一部分记录的关键码均小于或等于轴值,
后一部分记录的关键码均大于或等于轴值,
然后分别对这两部分重复上述方法,直到整个序列有序。
样例展示:
12 5 9 20 6 31 24
初始 | 12 | 5 | 9 | 20 | 6 | 31 | 24 |
l=0,r=n-1 temp=a[l]=12 | i | | | | | | j |
| 6 | 5 | 9 | 20 | 6 | 31 | 24 |
| i | | | | j | | |
| 6 | 5 | 9 | 20 | 20 | 31 | 24 |
| | | | i | j | | |
| 6 | 5 | 9 | 12 | 20 | 31 | 24 |
| | | j | i | | | |
l=0,r=i-1=2 temp=a[l]=6 | 6 | 5 | 9 | | | | |
| i | | j | | | | |
| 5 | 5 | 9 | | | | |
| i | j | | | | | |
| 5 | 6 | 9 | | | | |
| | j i | | | | | |
退出左边 | | | | | 20 | 31 | 34 |
l=i+1=5,r=6 temp=a[l]=20 | | | | | 20 | 31 | 34 |
| | | | | i | | j |
| | | | | 20 | 31 | 34 |
| | | | | ij | | |
l=i+1=5,r=6 temp=a[l]=20 | | | | | | 31 | 34 |
| | | | | | i | j |
| | | | | | 31 | 34 |
| | | | | | ij | |
ans=12 5 9 20 6 31 24
时间复杂度:
最好情况:
每一次划分对一个记录定位后,该记录的左侧子表与右侧子表的长度相同,为O(nlog2n)。
T(n)≤2T(n/2)+n
≤2(2T(n/4)+n/2)+n=4T(n/4)+2n
≤4(2T(n/8)+n/4)+2n=8T(n/8)+3n
… … …
≤nT(1)+nlog2n=O(nlog2n)
最坏情况:
每次划分只得到一个比上一次划分少一个记录的子序列(另一个子序列为空)
这样,总共需要进行n-1趟排序
第i趟排序又要进行为 n-i次比较
平均情况:为O(nlog2n)。
代码实现
//快速排序
void QuickSort(int a[],int l,int r)
{
if(l<r)
{
int temp=a[l];
int i=l,j=r;
while(i<j)
{
while(i<j&&a[j]>temp)
j--;
if(i<j) a[i++]=a[j];
while(i<j&&a[i]<temp)
i++;
if(i<j) a[j--]=a[i];
}
a[i]=temp;
QuickSort(a,l,i-1);
QuickSort(a,i+1,r);
}
}
选择排序
基本思想:
每趟排序在当前待排序序列中选出关键码最小的记录,添加新的最左边。
简单选择排序
基本思想:第i 趟在n-i+1(i=1,2,…,n-1)个记录中选取关键码最小的记录作为有序序列中的第i个记录。
样例展示:
12 5 9 20 6 31 24
5 [12 9 20 6 31 24]
5 6 [9 20 12 31 24]
5 6 9 [20 12 31 24]
5 6 9 12 [20 31 24]
5 6 9 12 20 [31 24]
5 6 9 12 20 24 31
时间复杂度:
移动次数:
最好情况(正序):0次
最坏情况:3(n-1)次
比较次数:
简单选择排序的时间复杂度为O(n^2)。
稳定性:稳定(严蔚敏)/不稳定(殷人昆)的排序算法。(意见不统一)
代码实现:
//选择排序
void SelectSort(int a[],int n)
{
for(int i=0;i<n-1;i++)
{
int index=i;
int j;
for(j=i+1;j<n;j++)
{
if(a[j]<a[index])
{
index=j;
}
}
if(index!=i) swap(a[index],a[i]);
}
}
堆排序
定义:
堆是具有下列性质的完全二叉树:每个结点的值都小于或等于其左右孩子结点的值(称为小根堆),或每个结点的值都大于或等于其左右孩子结点的值(称为大根堆)。
基本思想:
首先将待排序的记录序列构造成一个堆(大顶堆),此时,选出了堆中所有记录的最大者,然后将它从堆中移走,并将剩余的记录再调整成堆,这样又找出了次大的记录,以此类推,直到堆中只有一个记录。
样例展示:
初始化堆:
调整剩余记录,使其成为一个新堆
时间复杂度:
建堆:O(n)
删除堆顶的调整:O(log2n)
一次建堆 ,n次删除堆顶
总时间代价为O(nlog n)
空间代价为O(1)
不稳定的排序方法
代码实现:
//堆排序
//下标从1开始,因为这样才满足i的左孩子是2i与右孩子2i+1
void sift(int a[],int k,int n)
{
int i=k,j=2*i,temp=a[i]; //保存筛选记录
while(j<n)
{
while(j<n&&a[j]<a[j+1])j=j+1;//选左右孩子的最大的那一个
if(temp>a[j]) break;
else
{
a[i]=a[j];
i=j;
j=2*i;//找该孩子的左孩子
}
}
a[i]=temp; //上面i=j
}
void HeapSort(int a[],int n)
{
for(int i=n/2;i>=1;i--)//初建堆
{
sift(a,i,n);
}
for(int i=1;i<n;i++)
{
swap(a[1],a[n-i+1]);//移走堆顶
sift(a,1,n-i);//重建堆
}
}
归并排序
归并:将两个或两个以上的有序序列合并成一个有序序列的过程。
二路归并排序(非递归)
基本思想:
将一个具有n个待排序记录的序列看成是n个长度为1的有序序列,然后进行两两归并,得到n/2个长度为2的有序序列,再进行两两归并,得到n/4个长度为4的有序序列,……,直至得到一个长度为n的有序序列为止。
样例展示:
//归并排序
//将有二个有序数列a[first...mid]和a[mid...last]合并。
void mergearray(int a[],int first,int mid,int last,int temp[])
{
int i=first,j=mid+1;
int n=mid,m=last;
int k=0;
while(i<=n&&j<=m)
{
if(a[i]<=a[j])
{
temp[k++]=a[i];
i++;
}
else
{
temp[k++]=a[j];
j++;
}
}
while(i <= n)
temp[k++] = a[i++];
while (j <= m)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[first + i] = temp[i];
//out(a,k+first);
}
//非递归版本
void MergePass (int r[ ], int r1[ ], int n, int h)
{
int i=1; //第一个子序列的第一个元素
while (i<=n-2*h+1) //情况1
{
mergearray(r,i, i+h-1, i+2*h-1,r1);
i+=2*h;
}
if (i<n-h+1) mergearray (r, i, i+h-1, n,r1); //情况2
else for (int k=i; k<=n; k++) //情况3
r1[k]=r[k];
}
void MergeSort(int r[], int r1[], int n )
{
int h=1;
while (h<n)
{
MergePass (r, r1, n, h);
h=2*h;
MergePass (r1, r, n, h);
h=2*h;
}
}
二路归并排序(递归版本)
样例展示
//归并排序
//将有二个有序数列a[first...mid]和a[mid...last]合并。
void mergearray(int a[],int first,int mid,int last,int temp[])
{
int i=first,j=mid+1;
int n=mid,m=last;
int k=0;
while(i<=n&&j<=m)
{
if(a[i]<=a[j])
{
temp[k++]=a[i];
i++;
}
else
{
temp[k++]=a[j];
j++;
}
}
while(i <= n)
temp[k++] = a[i++];
while (j <= m)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[first + i] = temp[i];
//out(a,k+first);
}
//递归版本
void mergesort(int a[], int first, int last, int temp[])
{
if (first < last)
{
int mid = (first + last) >>1;
mergesort(a, first, mid, temp); //左边有序
mergesort(a, mid + 1, last, temp); //右边有序
mergearray(a, first, mid, last, temp); //再将二个有序数列合并
}
}
分配排序
基本思想
先将数据分配到不同的桶中
再将桶中的数据收集到一起
两种方法
桶式排序(单关键字排序)
链式基数排序(多关键字排序)
特点
排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序
它们的时间复杂度可达到线性阶:O(n)。
桶式排序(单关键字排序)
样例展示
特点:
需要较多的桶
时间复杂性
一次分配,O(n)
一次收集,O(m)
O(n+m)
空间复杂性
O(m)
稳定性
稳定
//桶排序
struct Node
{
int data;
Node * next;
};
struct head
{
Node *first, *rear;
};
void distribute(Node *first, int n, head *list)
{
Node *p,*q;
p=first;
int data;
while(p)
{
data=p->data;
q=p->next;
if( list[data].first)
{
list[data].rear->next=p;
list[data].rear=p;
}
else
list[data].first=list[data].rear=p;
list[data].rear->next=NULL;
p=q;
}
}
void collect( head *list, Node *&first,int m)
{
int i=0,j;
while(list[i].first==NULL)
i++;
if(i>m)
return;
first=list[i].first;
while(i<=m)
{
j=i+1;
while(list[j].first==NULL)
j++;
if(j>m)
return;
list[i].rear->next=list[j].first;
i=j;
}
}
//m为a[i]元素的最大值
void BucketSort(int a[],int n,int maxx)
{
Node *s,*first;
head *list;
list=new head[maxx];
for (int i=0; i<=maxx; i++)
{
list[i].first=NULL;
list[i].rear=NULL;
}
first=NULL;
for(int i=0; i<n; i++)
{
s=new Node;
s->data=a[i];
s->next=first;
first=s;
}
distribute(first,n,list);
collect(list,first,maxx);
Node *p=first;
while(p)
{
cout<<p->data<<" ";
p=p->next;
}
cout<<endl;
}
链式基数排序(多关键字排序)
样例展示(LSD)
来自菜鸟:https://www.runoob.com/w3cnote/radix-sort.html
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int maxData = data[0]; ///< 最大数
/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
/* int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;*/
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}
总结
四类算法比较
所有排序方法可分为两类,
(1)一类是稳定的,包括直接插入排序、起泡排序、和归并排序,基数桶式排序;
(2)另一类是不稳定的,包括直接选择排序、希尔排序、快速排序和堆排序。