0 排序算法概述
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
1 插入排序——直接插入排序(Straight Insertion Sort)
1.1算法思想
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
1.2算法复杂度
a. 时间复杂度:最好O(n),最坏O(n2)
1.3算法稳定性
如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
1.4算法实现
void straightInsertionSort(vector<int> &vec)
{
int sentry = vec[0], j;//哨兵
for (int i = 1;i < vec.size(); ++i)
{
if (vec[i] < vec[i-1])
{
sentry = vec[i];
j = i;
do
{
--j;
vec[j+1] = vec[j];//后移
} while(vec[j] > sentry);
vec[j+1] = sentry;
}
}
}
2 插入排序——希尔排序(Shell’s Sort)
2.1算法思想
希尔排序是将待排序列(R1, R2, R3, …, Rn)按照增量d划分为d个子序列,其中第i个子序列为(Ri, Ri+d, … Ri+kd),分别对子序列进行直接插入排序。
直接插入排序每次最多移动一个位置,希尔排序则是每次对相隔较远的距离进行比较,使得能够移动时跨越多个记录,实现宏观调整。希尔排序最后一步增量为1,此时序列基本有序,最后一趟对整个序列进行一次直接插入排序,效率比较高。
2.2算法复杂度
a.时间复杂度:希尔排序的时间复杂度和增量序列有关,最差O(n),最好O(nlogn)
b.空间复杂度:O(1)
2.3算法稳定性
希尔排序是一种不稳定的排序算法
2.4算法实现
void shellinsert(vector<int> &vec, int dk)
{
int sentry = 0, j;
for (int i = dk;i < vec.size(); ++i)
{
if (vec[i] < vec[i-dk])
{
sentry = vec[i];
j = i - dk;
vec[i] = vec[j];
while (j >= 0 && sentry < vec[j])
{
vec[j+dk] = vec[j];
j -= dk;
}
vec[j+dk] = sentry;
}
}
}
void shellsort(vector<int> &vec, int d[], int n)
{
for (int i = 0;i < n; ++i)
shellinsert(vec, d[i]);
}
3 选择排序——简单选择排序(Simple Selection Sort)
3.1算法思想
对比数组中前一个元素跟后一个元素的大小,如果后面的元素比前面的元素小则用一个变量k来记住他的位置,接着第二次比较,前面“后一个元素”现变成了“前一个元素”,继续跟他的“后一个元素”进行比较如果后面的元素比他要小则用变量k记住它在数组中的位置(下标),等到循环结束的时候,我们应该找到了最小的那个数的下标了,然后进行判断,如果这个元素的下标不是第一个元素的下标,就让第一个元素跟他交换一下值,这样就找到整个数组中最小的数了。然后找到数组中第二小的数,让他跟数组中第二个元素交换一下值,以此类推。
3.2算法复杂度
a.时间复杂度:最好O(n2),最坏O(n2),平均O(n2)
b.空间复杂度:O(1)
3.3算法稳定性
不稳定
3.4算法实现
void selectionsort(vector<int> &vec)
{
int k = 0;
for (int i = 0;i < vec.size() - 1; ++i)
{
k = i;
for (int j = i + 1; j < vec.size(); ++j)
{
if (vec[k] > vec[j])
k = j;
}
if (i != k)
swap(vec[i], vec[k]);
}
}
4 选择排序——堆排序(Heap Sort)
5 交换排序——冒泡排序(Bubble Sort)
5.1算法思想
冒泡排序算法的运作如下:(从后往前)
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的
3.最后一对。在这一点,最后的元素应该会是最大的数.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
5.2算法复杂度
a.时间复杂度:最好O(n),最坏O(n2),平均O(n2)
b.空间复杂度:O(1)
5.3算法稳定性
稳定
5.4算法实现
void bubblesort(vector<int> &vec)
{
for (int i = 0;i < vec.size() - 1; ++i)
{
for (int j = i + 1; j < vec.size(); ++j)
{
if (vec[i] > vec[j])
swap(vec[i], vec[j]);
}
}
}
6 交换排序——快速排序(Quick Sort)
6.1 算法原理
快速排序是一种采用分治策略的排序算法,其基本思想时:
- 从数组中选出一个数
- 把比这个数大的放在右边,小的放在左边
- 对两边区间进行同样的处理
6.2 算法实现
对于一个区间[l, r],令i = l, j = r。每次先从第一个数即a[l]开始,将其作为轴,用X记录值,先从后面j开始,找到第一个比X小的数,填充到a[l],l++,然后从前面i开始找,找到第一个比X大的数,填充到a[j],j–。这样子就得到了新的[i, j]区间,区间外左边的数都比X小,区间外右边的数都比X大。然后重复以上过程,直到i==j也就是区间[i, j]只剩下一个数,这个数满足数组左边比他小,数组右边比他大。这样子就找到了第一个分割点。
对分割点左右两边区间进行同样的操作,直到左右两边的区间都是一个数,算法结束。
- 基准数的选择
一般是选择第一个作为基准,但是这样子其实是很糟糕的,对于有序序列反而没有帮助。因此一般基准都是按照三数中值分割法来选择:取左右中三个数的中值作为基准数。
//返回调整后基准数位置
int adjust(int s[], int l, int r)
{
int i = l, j = r;
int x = s[l];
while (i < j)
{
while (i < j && s[j] >= x)
--j;
if (i < j) s[i++] = s[j];
while (i < j && s[i] <= x)
++i;
if (i < j) s[j--] = s[i];
}
s[i] = x;
return i;
}
void quickSort(int s[], int l, int r)
{
if (l < r)
{
int index = adjust(s, l, r);
quickSort(s, l, index - 1);
quickSort(s, index + 1, r);
}
}
6.3 算法复杂度
- 时间复杂度:最好O(nlogn),最坏O(n2),当数组是有序是,快速排序退化成冒泡排序
- 空间复杂度:O(1)
6.4算法稳定性
快速排序时不稳定的排序算法
7 归并排序(Merge Sort)
7.1算法原理
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
7.2算法实现
首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
void merge(const vector<int> &v1, const vector<int> &v2, vector<int> &v)
{
int i = 0,j = 0, k = 0;
while (i < v1.size() && j < v2.size())
{
if (v1[i] < v2[j])
v[k++] = v1[i++];
else
v[k++] = v2[j++];
}
while (i < v1.size()) v[k++] = v1[i++];
while (j < v2.size()) v[k++] = v2[j++];
}
可以看出合并有序数列的效率是比较高的,可以达到O(n)。
- 归并排序的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
//[beg, mid] (mid, end]归并
void merge(vector<int> &v, int beg, int mid, int end, vector<int> &temp)
{
int i = beg, j = mid + 1, k = 0;
while (i <= mid && j <= end)
{
if (v[i] <= v[j])
temp[k++] = v[i++];
else
temp[k++] = v[j++];
}
while (i <= mid) temp[k++] = v[i++];
while (j <= end) temp[k++] = v[j++];
for (int l = 0;l < k; ++l)
v[beg + l] = temp[l];
}
//[beg, end]排序
void msort(vector<int> &v, int beg, int end, vector<int> &temp)
{
if (beg < end)
{
int mid = (beg + end) / 2;
msort(v, beg, mid, temp);
msort(v, mid+1, end, temp);
merge(v, beg, mid, end, temp);
}
}
void mergesort(vector<int> &v)
{
vector<int> temp(v.size());
msort(v, 0, v.size() - 1, temp);
}
7.3算法稳定性
归并时,如果是判断if (v[i] <= v[j])
则是稳定的,若为if (v[i] < v[j])
,则不稳定。
7.4算法复杂度
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
8 桶排序/基数排序(Radix Sort)
8.1算法思想
桶排序是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
- 例如要对大小为[1..1000]范围内的n个整数A[1..n]排序
1. 首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储(10..20]的整数,……集合B[i]存储( (i-1)*10, i*10]的整数,i = 1,2,..100。总共有 100个桶。
2. 然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。
3. 最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
8.2算法复杂度
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m)) = O(n + nlogn - nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n).
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
8.3算法缺点
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
- 首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
- 其次待排序的元素都要在一定的范围内等等。