全文目录
- 排序的概念
- 排序的概念
- 内外排序
- 常见排序算法
- 稳定性
- 插入排序
- 算法思想
- 实现
- 特性总结
- 希尔排序
- 算法思想
- 实现
- 特性总结
- 选择排序
- 算法思想
- 实现
- 特性总结
- 冒泡排序
- 算法思想
- 实现
- 特性总结
- 堆排序
- 算法思想
- 实现
- 特性总结
- 快速排序
- 算法思想
- hoare版
- 挖坑法
- 前后指针法
- 优化
- 精简版
- 非递归实现
- 特性总结
- 归并排序
- 算法思想
- 递归实现
- 非递归实现
- 特性总结
- 计数排序
- 算法思想
- 实现
- 特性总结
- 基数排序
- 算法思想
- 实现
- 特性总结
将一组“无序”的记录序列调整为“有序”(递增或者递减)的记录序列。
内部排序:整个排序过程不需要访问外存便能完成
外部排序:若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成
常见的排序算法都是比较类排序算法:
稳定排序:假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字( a = = b , 且 a 在 b 前面 a == b, 且a在b前面 a==b,且a在b前面),在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变( a 依旧在 b 前面 a依旧在b前面 a依旧在b前面),则这种排序方法是稳定的。
其中冒泡,插入,基数,归并属于稳定排序,选择,快速,希尔,归属于不稳定排序。
插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增 1 1 1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
// 插入排序
void InsertSort(int* a, int n)
{
assert(a);
for (int i = 1; i < n; i++)
{
int t = a[i];
int j = i - 1;
while (j >= 0 && a[j] < t)
{
a[j + 1] = a[j];
j--;
}
a[j + 1] = t;
}
}
稳定性: 稳定
时间复杂度:
空间复杂度: O ( 1 ) O(1) O(1)
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
这里我们采取Knuth的方法: g a p = ⌊ g a p / 3 ⌋ + 1 gap = \lfloor gap / 3 \rfloor + 1 gap=⌊gap/3⌋+1
其中增量越大,越无序,排序次数越小,增量越小越有序,排序次数越多。一般的增量一开始取 n n n,随后每次 / 3 / 3 /3,直到为1
// 希尔排序
void ShellSort(int* a, int n)
{
int grap = n;
while (grap > 1)
{
grap = grap / 3 + 1;
for (int i = grap; i + grap - 1 < n; i++)
{
int t = a[i];
int j = i - grap;
while (j >= 0 && a[j] > t)
{
a[j + grap] = a[j];
j -= grap;
}
a[j + grap] = t;
}
}
}
稳定性: 不稳定
空间复杂度: O ( 1 ) O(1) O(1)
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
优化: 找最大(小)元素的的同时找到最小(大)元素,一次解决两个元素。
// 选择排序
void SelectSort(int* a, int n)
{
assert(a);
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = end;
for (int i = begin; i <= end; i++)
{
if (a[maxi] < a[i]) maxi = i;
if (a[mini] > a[i]) mini = i;
}
Swap(&a[begin], &a[mini]);
if (begin == maxi) maxi = mini; // 防止maxi与begin重叠
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
时间复杂度:
空间复杂度: O ( 1 ) O(1) O(1)
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
优化: 如果一趟排序中没有出现交换的情况说明已经有序,提前结束。
// 冒泡排序
void BubbleSort(int* a, int n)
{
assert(a);
for (int i = 0; i < n; i++)
{
bool flag = true;
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = false;
}
}
if (flag) break;
}
}
稳定性: 稳定
时间复杂度:
空间复杂度: O ( 1 ) O(1) O(1)
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
利用堆的删除思想:
向下调整: 每次取左右孩子中较大(小) 的元素,如果其比父节点要大(小) 交换两者的值, 依次向下迭代,直到子节点超出堆底。
// 堆排序
// 向下调整算法
void AdjustDwon(int* a, int n, int parent)
{
assert(a);
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
child++;
if (a[child] > a[parent])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
void HeapSort(int* a, int n)
{
assert(a);
// 建堆
for (int i = n - 1 - 1 / 2; i >= 0; i--)
AdjustDwon(a, n, i);
while (n > 0)
{
Swap(&a[0], &a[--n]);
AdjustDwon(a, n, 0);
}
}
稳定性: 不稳定
时间复杂度:
空间复杂度: O ( 1 ) O(1) O(1)
快速排序(Quicksort),计算机科学词汇,适用领域Pascal,c++等语言,是对冒泡排序算法的一种改进。
设要排序的数组是 A [ 0 ] … … A [ N − 1 ] A[0]……A[N-1] A[0]……A[N−1],首先任意选取一个数据(通常选
用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
一趟快速排序的算法是:
void PartSort1(int* a, int left, int right)
{
assert(a);
if (left >= right) return;
int key = a[left];
int l = left, r = right;
while (l < r)
{
while (l < r && a[r] >= key) r--;
while (l < r && a[l] <= key) l++;
Swap(&a[l], &a[r]);
}
Swap(&a[left], &a[r]);
PartSort1(a, left, r - 1);
PartSort1(a, r + 1, right);
}
注意:
当
key
取左边界时,需要让右边先走,这样才能保证left
和right
是相遇的位置是小于key
的
当
key
取右边界时,需要让左边先走,这样才能保证left
和right
是相遇的位置是大于key
的
在hoare版上进行改进,好处是对于最后的相遇更好理解,但还是要按照规则来。
void PartSort2(int* a, int left, int right)
{
assert(a);
if (left >= right) return;
int piti = left;
int key = a[left];
int l = left, r = right;
while (l < r)
{
while (l < r && a[r] >= key) r--;
a[piti] = a[r];
piti = r;
while (l < r && a[l] <= key) l++;
a[piti] = a[l];
piti = l;
}
a[piti] = key;
PartSort2(a, left, l - 1);
PartSort2(a, l + 1, right);
}
前后指针法,不再是从左右两边往中间走,而是两个指针同时从头向尾走,这算法可以不用考虑最后一次相遇的情况。
单趟思路:
key
取左端点key
的值key
和前指针的值// 快速排序前后指针法
void PartSort3(int* a, int left, int right)
{
assert(a);
if (left >= right) return;
int key = a[left];
int cur = left + 1, pre = left;
while (cur <= right)
{
if (a[cur] < key && ++pre != cur)
Swap(&a[cur], &a[pre]);
cur++;
}
Swap(&a[pre], &a[left]);
PartSort3(a, left, pre - 1), PartSort3(a, pre + 1, right);
}
快速排序最大影响是key
的取值和递归的消耗,所以对于快速排序最大的优化就是对key
的取值和递归的优化。
三数取中优化:
快速排序最大的影响点是key
,如果每次key
取得是序列中的最大值或者最小值,那么整个排序的时间复杂度就是 O ( n 2 ) O(n^2) O(n2),所以我们可以加一些优化,使得key
不至于取到最值。
key
取 left, right, mid
三个数中的中间值,这样的即可避免key
取最值:
// 三数取中
int GetMid(int* a, int left, int right)
{
int mid = left + right >> 1;
if (a[left] < a[mid])
{
if (a[mid] < a[right]) return mid;
else if (a[right] < a[left]) return left;
else return right;
}
else // a[mid] <= a[left]
{
if (a[left] < a[right]) return left;
else if (a[right] < a[left]) return right;
else return mid;
}
}
小区间优化法:
当区间比较小时,如果还是进行快速排序的话递归的消耗会比使用其他排序算法的消耗更高,所以当区间较小时,我们可以使用其他的插入排序来改善这种情况:
if (right - left < 20)
InsertSort(a, left, right);
else
QuickSort(a, left, right);
具体的详情看这里:快速排序精简版
void QuickSort(int* a, int left, int right)
{
if (left >= right) return;
int i = left - 1, j = right + 1, x = a[left + right >> 1];
while (i < j)
{
do i++; while (a[i] < x);
do j--; while (a[j] > x);
if (i < j) Swap(&a[i], &a[j]);
}
QuickSort(a, left, j), QuickSort(a, j + 1, right);
}
如果递归程度太深会导致栈溢出,所以我们可以使
非递归快速排序我们需要借助栈来模拟函数栈帧,栈用来维护排序区间。
typedef struct Range
{
int l, r;
} Range;
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
assert(a);
if (left >= right) return;
Stack st;
StackInit(&st);
Ranges range = { left, right };
StackPush(&st, range);
while (!StackEmpty(&st))
{
range = StackTop(&st);
StackPop(&st);
if (range.l >= range.r) continue; // 区间不存在
// 处理当前区间
int i = range.l - 1, j = range.r + 1, x = a[range.l + range.r >> 1];
while (i < j)
{
do i++; while (a[i] < x);
do j--; while (a[j] > x);
if (i < j) Swap(&a[i], &a[j]);
}
// 加入新区间
Ranges l = { range.l, j };
Ranges r = { j + 1, range.r };
StackPush(&st, l);
StackPush(&st, r);
}
}
稳定性: 不稳定
时间复杂度:
空间复杂度: O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)(递归的消耗)
归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并操作的工作原理如下:
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
// 归并排序递归实现
void MergeSort(int* a, int left, int right)
{
assert(a);
if (left >= right) return;
int mid = left + right >> 1;
MergeSort(a, left, mid), MergeSort(a, mid + 1, right);
int* b = (int*)malloc(sizeof(int) * (right - left + 1));
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right)
{
if (a[i] < a[j]) b[k++] = a[i++];
else b[k++] = a[j++];
}
while (i <= mid) b[k++] = a[i++];
while (j <= right) b[k++] = a[j++];
for (int i = 0, j = left; j <= right; j++, i++) a[j] = b[i];
free(b);
}
归并排序算是后续遍历,不能直接借助栈来实现,我们可以反过来,先处理区间长度为1的区间,然后区间长度依次上升,直到大于n:
// 归并排序非递归实现
void MergeSortNonR(int* a, int left, int right)
{
assert(a);
if (left >= right) return;
int n = right - left + 1;
int grap = 1;
int* b = (int*)malloc(sizeof(int) * (right - left + 1));
while (grap < n)
{
int j = 0;
for (int i = 0; i <= right; i += 2 * grap)
{
//[i, i + grap - 1], [i + grap, i + 2 * grap - 1]
int begin1 = i, end1 = i + grap - 1;
int begin2 = i + grap, end2 = i + 2 * grap - 1;
// 区间修正,防止越界
if (end1 > right)
{
end1 = right;
begin2 = right + 1, end2 = right;
}
else if (begin2 > right)
{
begin2 = right + 1, end2 = right;
}
else if (end2 > right)
{
end2 = right;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2]) b[j++] = a[begin1++];
else b[j++] = a[begin2++];
}
while (begin1 <= end1) b[j++] = a[begin1++];
while (begin2 <= end2) b[j++] = a[begin2++];
}
grap *= 2;
memcpy(a, b, sizeof(int) * n);
}
free(b);
}
稳定性: 稳定
时间复杂度:
空间复杂度: O ( n ) O(n) O(n)
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为 O ( n + k ) Ο(n+k) O(n+k)(其中 k k k是整数的范围),快于任何比较排序算法。 [1] 当然这是一种牺牲空间换取时间的做法,而且当 O ( k ) > O ( n ∗ l o g ( n ) ) O(k)>O(n*log(n)) O(k)>O(n∗log(n)) 的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是 O ( n ∗ l o g ( n ) ) O(n*log(n)) O(n∗log(n)), 如归并排序,堆排序)
range
range
大小的计数序列,并将原序列中的值映射到计数序列中// 计数排序
void CountSort(int* a, int n)
{
assert(a);
int min = a[0], max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] > max) max = a[i];
if (a[i] < min) min = a[i];
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
memset(count, 0, sizeof(count) * range);
for (int i = 0; i < n; i++) count[a[i] - min]++;
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--) a[j++] = i + min;
}
free(count);
}
稳定性: 稳定
时间复杂度:
空间复杂度: O ( r a n g e ) O(range) O(range)
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为 O ( n l o g r m ) O (nlog^rm) O(nlogrm),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
#define RADIX 10
#define K 3
queue<int> q[RADIX];
int GetKey(int a, int k)
{
for (int i = 0; i < k; i++) a /= 10;
return a % 10;
}
void Distribute(int* a, int left, int right, int k)
{
for (int i = left; i < right; i++)
{
int key = GetKey(a[i], k);
q[key].push(a[i]);
}
}
void Collect(int* a, int left, int right)
{
int j = 0;
for (int i = 0; i < RADIX; i++)
{
while (!q[i].empty())
{
a[j++] = q[i].front();
q[i].pop();
}
}
}
void RadixSort(int* a, int left, int right)
{
for (int i = 0; i < K; i++)
{
Distribute(a, left, right, i);
Collect(a, left, right);
}
}
稳定性: 稳定
时间复杂度:
空间复杂度: O ( r a d i x + n ) O(radix + n) O(radix+n)