您的位置:首页 > 理论基础 > 数据结构算法

数据结构期末复习:排序算法

2017-12-12 16:18 429 查看
图片来源toptal

本文用几种排序算法演示对一个大小为20的数组排序,主要参考课本《C++数据结构与算法(第四版)》,数组下标默认从0开始。

插入排序

时间复杂度O(n2)

空间复杂度O(1)

先对前i个数排好序,对于第i+1个数,不断将前面的数后移1位,直到找到合适的位置插进去。



# include <iostream>
using namespace std;
void insertion_sort(int a[], int n)
{
for(int i=1; i<n; ++i)
{
int j, tmp = a[i];
for(j=i; j>0 && a[j-1]>tmp; --j)
a[j] = a[j-1];
a[j] = tmp;
}
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
insertion_sort(a, n);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}


选择排序

时间复杂度O(n2)

空间复杂度O(1)

将第i个数分别和第[i+1,n−1]个数比较,将最小的数交换到i处。



# include <iostream>
using namespace std;
void selection_sort(int a[], int n)
{
for(int i=0; i<n; ++i)
{
int tmp = i;
for(int j=i+1; j<n; ++j)
if(a[j] < a[tmp])
tmp = j;
swap(a[tmp], a[i]);
}
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
selection_sort(a, n);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}


冒泡排序

时间复杂度O(n2)

空间复杂度O(1)

从后往前比较相邻两位,不断将较小值交换到前面。



# include <iostream>
using namespace std;
void bubble_sort(int a[], int n)
{
for(int i=0; i<n; ++i)
for(int j=n-1; j>i; --j)
if(a[j-1] > a[j])
swap(a[j-1], a[j]);
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
bubble_sort(a, n);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}


希尔排序

时间复杂度下界O(n∗log2n),与增量序列的选取有关

空间复杂度O(1)

一般采用插入排序,算是插入排序的升级版,先将数组分成几个子数组,对相隔较远的元素进行插入排序,再对相隔较近的元素进行插入排序,通过广泛的研究,这个间隔为代码中的increments数组比较合适。



# include <iostream>
using namespace std;
int increments[23]={1};
void shell_sort(int a[], int n)
{
for(int i=1; i<n; ++i) increments[i] = 3*increments[i-1] + 1;//设置增量值
for(int i=n-1; i>=0; --i)
{
int h = increments[i];//当前增量值
for(int j=h; j<2*h; ++j)//h到2h的位置都要跑一遍
{
for(int k=j; k<n; k+=h)
{
int tmp = a[k];
int pos = k;
while(pos-h >= 0 && a[pos-h] > tmp)//类似插入排序
{
a[pos] = a[pos-h];
pos -= h;
}
a[pos] = tmp;
}
}
}
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
shell_sort(a, n);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}


堆排序

时间复杂度O(nlogn)

空间复杂度O(1)

首先介绍大根堆:

1.每一个节点的值都大于等于它儿子的值。

2. 该二叉树完全平衡,即最后一层叶子结点位于最左侧位置。

于是堆顶元素一定是最大的,利用此性质每次将堆顶元素交换到数组的最后(类似选择排序的相反版本),然后恢复堆,重复这一过程实现对数组的升序排序。

用数组实现堆参考课本P215。



# include <iostream>
using namespace std;
void movedown(int a[], int left, int right)//将根元素沿树向下移动
{
int big_son = 2*left+1;
while(big_son <= right)
{
if(big_son<right && a[big_son] < a[big_son+1]) ++big_son;//找出较大的那个儿子
if(a[left] < a[big_son])//如果爸爸比儿子小
{
swap(a[left], a[big_son]);//就执行交换
left = big_son;//然后继续更新儿子
big_son = left*2+1;
}
else break;//否则可以退出了
}
}
void heap_sort(int a[], int n)
{
for(int i=n/2-1; i>=0; --i)//建堆
movedown(a, i, n-1);
for(int i=n-1; i>=0; --i)
{
swap(a[0], a[i]);//将最值放到末尾
movedown(a, 0, i-1);//恢复堆,即重新将最值放到堆顶部
}
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
heap_sort(a, n);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}


快速排序

时间复杂度O(nlogn)

空间复杂度O(log2n)

将数组分为两个子数组,设置一个基准值,使得左边数组小于等于该基准值,右边数组大于等于该基准值,对两个子数组递归此过程实现升序排序。

课本中快排之前进行预处理将最大值放到数组末尾,原因就是课本的代码首先将基准值换到数组的首位,最后再换到正确的位置,那么第一次快排时如果最大值刚好被换到首位,low指针就会永无止境地加下去导致数组越界,因此该预处理是必要的。



# include <iostream>
using namespace std;
void qucik_sort(int a[], int left, int right)
{
swap(a[left], a[(left+right)/2]);//先将基准元素放到前面,防止它来回移动(swap(a[low],a[up])),结束再放回正确的位置。
int low = left+1, up = right, bound = a[left];
while(low <= up)
{
while(a[low] < bound) ++low;
while(a[up] > bound) --up;
if(low < up)
{
swap(a[low], a[up]);//将左边>=基准值和右边<=基准值的数字交换
++low;--up;
}
else break;
}
swap(a[up], a[left]);//将基准值放回去正确位置,显然可以是up所在位置
if(left < up-1) qucik_sort(a, left, up-1);//递归对左子数组排序,up为分界点,a[up]不用排
if(right > up+1) qucik_sort(a, up+1, right);//递归对右子数组排序,up为分界点
}
void quick_sort(int a[], int n)
{
if(n < 2) return;
int imax = 0;
for(int i=1; i<n; ++i)//预处理,将最大的元素调到数组最后,否则第一次调用快排可能会使low指针越界
if(a[i] > a[imax])
imax = i;
swap(a[n-1], a[imax]);
qucik_sort(a, 0, n-2);
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
quick_sort(a, n);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}


归并排序

时间复杂度O(nlogn)

空间复杂度O(n)

将一个数组拆成两个子数组,分别排序后再合并在一起,这是一个递归的过程。因为两个子数组已经有序,合并操作可以比较快的完成。



# include <iostream>
using namespace std;
int temp[20];//临时数组
void merge(int a[], int left, int right)
{
int cnt = 0, mid = (left+right)/2;
int l=left, r=mid+1;//l和r分别是左子数组的开头和右子数组的开头
while(l<=mid && r<=right)//如果两个子数组均有元素
{
if(a[l] < a[r]) temp[cnt++] = a[l++];//按升序放到temp数组
else temp[cnt++] = a[r++];//按升序放到temp数组
}
while(l <= mid) temp[cnt++] = a[l++];//将左子数组剩余元素放到temp数组,当然本循环不会和下面的循环同时出现
while(r <= right) temp[cnt++] = a[r++];//将右子数组剩余元素放到temp数组,当然本循环不会和上面的循环同时出现
cnt = 0;
for(int i=left; i<=right; ++i)//更新整个数组
a[i] = temp[cnt++];
}
void merge_sort(int a[], int left, int right)
{
if(left >= right) return;
int mid = (left+right)/2;
merge_sort(a, left, mid);//对左子数组排序
merge_sort(a, mid+1, right);//对右子树组排序
merge(a, left, right);//合并两个子数组,因为他们已经别排序了,合并操作只需O(right-left+1)复杂度
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
merge_sort(a, 0, n-1);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}


基数排序

复杂度O(nlog(r)m),r为所采取的基数,m为堆数

比较神奇的算法,平常我们判断两个数的大小是先从最高位开始判断的,最高位相同就按剩下的低位数判断。假如我们只按高位排序,有没有办法使得低位也自动排好序呢?基数排序就是这个原理,它从最低位开始,对每一个独立的数位进行排序,具体就是放到一个二维队列里面(维度一般为10,因为单个数位只有0到9),队列是线性结构,按顺序放进去必然能保持数据的有序性,所以同一个队列里面的数值永远都是“有序”的,根据这个性质就能实现升序排序了。

# include <iostream>
# include <queue>
using namespace std;
void radix_sort(int a[], int n)
{
int digits = 2, radix = 10;//digits是最长那个数字的长度,radix是单个数位的范围。
queue<int>q[radix];
for(int i=0, fac=1; i<digits; ++i,fac*=10)
{
for(int j=0; j<n; ++j)
q[(a[j]/fac)%10].push(a[j]);//按当前数位的大小进队
for(int j=0,cnt=0; j<radix; ++j)//从小到大更新原数组
{
while(!q[j].empty())
{
a[cnt++] = q[j].front();
q[j].pop();
}
}
}
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
radix_sort(a, n);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}


计数排序

复杂度O(n+k),k是整数范围

比较巧妙的思路,当k不是特别大时效率很高。算法用到两个数组count和tmp,count[i]记录i这个数出现了几次,然后count数组最后要求一次前缀和,那么此时count[i]就表示i这个数在n个数中排第几位了,接下来就好办了,tmp数组是临时存放结果的数组。

# include <iostream>
using namespace std;
int count[100], tmp[100];
void counting_sort(int a[], int n)
{
int biggest = 0;//最大的数
for(int i=0; i<n; ++i) biggest = max(biggest, a[i]);
for(int i=0; i<n; ++i) ++count[a[i]];
for(int i=1; i<=biggest; ++i) count[i] += count[i-1];//前缀和
for(int i=0; i<n; ++i)
{
tmp[count[a[i]]-1] = a[i];
--count[a[i]];
}
for(int i=0; i<n; ++i)
a[i] = tmp[i];
}
int main()
{
int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
int n = 20;
counting_sort(a, n);
for(int i=0; i<n; ++i)
cout << a[i] << " ";
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: