冒泡排序(n^2)
第一遍,从第一个元素开始, 相邻两个分别比较,大的放到后面。(最后一个元素是最大的)
第二遍,从第二个元素开始...
void Bubblesort(int *a, int n)
{
int i, j, k;
for (i = 1;i < n;i++)
{
for (j = 1;j <= n - i;j++)
{
if (a[j] > a[j + 1])
k = a[j], a[j] = a[j + 1], a[j + 1] = k;
}
}
}
选择排序(n^2)
第一个和之后的比较,谁大谁放第一个。
第二个和之后的比较,谁大谁放第二个。
void Selectionsort(int *a, int n)
{
int i, j, k;
for (i = 1;i <= n;i++)
{
for (j = i + 1;j <= n;j++)
{
if (a[i] > a[j])
k = a[i], a[i] = a[j], a[j] = k;
}
}
}
快速排序
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
复杂度:O(nlogn)
void quicksort(int *a, int l, int r)
{
int i = l, j = r, k = a[l];
if (l < r)
{
while (i < j)
{
while (i < j&&a[j] >= k)
j--;
if (i < j) a[i++] = a[j];
while (i < j&&a[i] <= k)
i++;
if (i < j) a[j--] = a[i];
}
a[i] = k;
quicksort(a, l, i - 1);
quicksort(a, i + 1, r);
}
}
插入排序—直接插入排序
相邻的两个元素a(index:i)和b(index:i+1),进行比较,如果a大于b,交换a,b位置,然后b继续去和a前面的元素比较,如果还是b小,继续交换位置。
void insertsort(int *a, int n)
{
for (int i = 0;i < n;i++)
{
int j = i - 1, tmp = a[i];
while (j >= 0 && tmp < a[j])
a[j + 1] = a[j], j--;
a[j + 1] = tmp;
}
}
插入排序-希尔排序
直接插入排序的进阶版。先在步长为个数一半进行插入排序,然后逐次递减。
void shellsort(int *a, int n)
{
int i, j, k, id, gap;
for (gap = n / 2;gap;gap /= 2)
{
for (i = gap;i <= n;i++)
{
for (j = i - gap;j >= 0 && a[j] > a[j + gap];j -= gap)
k = a[j], a[j] = a[j + gap], a[j + gap] = k;
}
}
}
归并排序 复杂度:O(nlogn)
归并操作(merge),也叫归并算法,指的是将两个顺序序列合并成一个顺序序列的方法。
如 设有数列{6,202,100,301,38,8,1}
初始状态:6,202,100,301,38,8,1
第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;
第二次归并后:{6,100,202,301},{1,8,38},比较次数:4;
第三次归并后:{1,6,8,38,100,202,301},比较次数:4;
总的比较次数为:3+4+4=11
速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列,应用见2011年普及复赛第3题“瑞士轮”的标程
//归并排序
func mergeSort(_ arr: inout [Int]) {
var gap = 1;
while gap < arr.count {
mergePass(&arr, gap: gap);
gap *= 2;
}
}
//分解合并序列,gap表示子序列的元素个数
func mergePass(_ arr: inout [Int], gap: Int) {
var i = 0;
let count = arr.count;
//设置初始排序的值如果为1,则相邻的两个排序即[1,2,3,5,6]排[{1,2},{3,4}...]
//之后继续四个排
while i + 2 * gap - 1 < count {
//0,0,1
mergeArray(&arr, low: i, mid: i + gap - 1, high: i + 2 * gap - 1);
i += 2 * gap;
}
//合并剩余的序列
if i + gap - 1 < count {
mergeArray(&arr, low: i, mid: i + gap - 1, high: count - 1);
}
}
//合并两个序列
func mergeArray(_ arr: inout [Int], low: Int, mid: Int, high: Int) {
var i = low;
var j = mid + 1;
var k = 0;
var array = Array<Int>(repeating: 0, count: high - low + 1);
while i <= mid && j <= high {
if arr[i] < arr[j] {
array[k] = arr[i];
i += 1;
k += 1;
} else {
array[k] = arr[j];
j += 1;
k += 1;
}
}
while i <= mid {
array[k] = arr[i];
i += 1;
k += 1;
}
while j <= high {
array[k] = arr[j];
j += 1;
k += 1;
}
//将排序好的序列复制回原数组
k = 0;
for i in low...high {
arr[i] = array[k];
k += 1;
}
}
var array = [2, 5, 8, 9, 10, 4, 3, 16, 1, 7, 8];
mergeSort(&array);
print(array);
计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。[1-2] 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlog(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n)), 如归并排序,堆排序)
它的原理是,计算数组的范围计入一个数组S,然后再统计数组S各个元素的个数。
然后根据S里面各个数量整理出排序后的数组。如:
nums=[2, 1, 3, 1, 5] , 首先扫描一遍获取最小值和最大值, maxValue=5 , minValue=1 ,于是开一个长度为5的计数器数组 counter ,
- 分配。统计每个元素出现的频率,得到 counter=[2, 1, 1, 0, 1] ,例如 counter[0] 表示值 0+minValue=1 出现了2次。
- 收集。 counter[0]=2 表示 1 出现了两次,那就向原始数组写入两个1, counter[1]=1 表示 2 出现了1次,那就向原始数组写入一个2,依次类推,最终原始数组变为 [1,1,2,3,5] ,排序好了。
void cntsort(int *a, int n, int *s,int *Rank)
{
int i, j;
for (i = 1;i <= n;i++)
s[a[i]]++;
for (i = 1;i <= 100;i++)
s[i] += s[i - 1];
for (i = n;i >= 1;i--)
Rank[s[a[i]]--] = a[i];
for (i = 1;i <= n;i++)
printf("%d ", Rank[i]);
printf("\n");
}
桶排序
- 将待排序元素划分到不同的痛。先扫描一遍序列求出最大值 maxV 和最小值 minV ,设桶的个数为 k ,则把区间 [minV, maxV] 均匀划分成 k 个区间,每个区间就是一个桶。将序列中的元素分配到各自的桶。
- 对每个桶内的元素进行排序。可以选择任意一种排序算法。
- 将各个桶中的元素合并成一个大的有序序列。
- 假设数据是均匀分布的,则每个桶的元素平均个数为 n/k 。假设选择用快速排序对每个桶内的元素进行排序,那么每次排序的时间复杂度为 O(n/klog(n/k)) 。总的时间复杂度为 O(n)+O(m)O(n/klog(n/k)) = O(n+nlog(n/k)) = O(n+nlogn-nlogk 。当 k 接近于 n 时,桶排序的时间复杂度就可以金斯认为是 O(n) 的。即桶越多,时间效率就越高,而桶越多,空间就越大。
计数排序是一种特殊的桶排序。
基数排序
是一种非比较排序算法,时间复杂度是 O(n) 。它的主要思路是,
将所有待排序整数(注意,必须是非负整数)统一为位数相同的整数,位数较少的前面补零。一般用10进制,也可以用16进制甚至2进制。所以前提是能够找到最大值,得到最长的位数,设 k 进制下最长为位数为 d 。
-
从最低位开始,依次进行一次稳定排序。这样从最低位一直到最高位排序完成以后,整个序列就变成了一个有序序列。
举个例子,有一个整数序列,0, 123, 45, 386, 106,下面是排序过程:第一次排序,个位,000 123 045 386 106,无任何变化
第二次排序,十位,000 106 123 045 386
第三次排序,百位,000 045 106 123 386
最终结果,0, 45, 106, 123, 386, 排序完成。为什么同一数位的排序子程序要用稳定排序?因为稳定排序能将上一次排序的成果保留下来。例如十位数的排序过程能保留个位数的排序成果,百位数的排序过程能保留十位数的排序成果。能不能用2进制?能,可以把待排序序列中的每个整数都看成是01组成的二进制数值。那这样的话,岂不是任意一个非负整数序列都可以用基数排序算法?理论上是的,假设待排序序列中最大整数为2 4 . 1,则最大位数 d=64 ,时间复杂度为 O(64n) 。可见任意一个非负整数序列都可以在线性时间内完成排序。
既然任意一个非负整数序列都可以在线性时间内完成排序,那么基于比较排序的算法有什么意义呢?基于比较的排序算法,时间复杂度是 O(nlogn) ,看起来比 O(64n) 慢,仔细一想,其实不是, O(nlogn) 只有当序列非常长,达到2 个元素的时候,才会与 O(64n) 相等,因此,64这个常数系数太大了,大部分时候, n 远远小于2 ,基于比较的排序算法还是比 O(64n) 快的。
当使用2进制时, k=2 最小,位数 d 最大,时间复杂度 O(nd) 会变大,空间复杂度 O(n+k) 会变小。当用最大值作为基数时, k=maxV 最大, d=1 最小,此时时间复杂度 O(nd) 变小,但是空间复杂度 O(n+k) 会急剧增大,此时基数排序退化成了计数排序。
堆排序
略(回头再讲)