常见比较类排序算法总结和实现
排序的稳定性和复杂度
类别 | 排序方式 |
|
空间复杂度 | 稳定性 | ||
---|---|---|---|---|---|---|
平均情况 | 最好情况 | 最坏情况 | ||||
插入排序 | 直接插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N^1.3) | O(N) | O(N^2) | O(1) | 不稳定 | |
选择排序 | 直接选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 | |
交换排序 | 冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
快速排序 | O(N*logN) | O(N*logN) | O(N^2) | O(logN) | 不稳定 | |
归并排序 | 归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 稳定 |
每种排序原理和实现
比较类排序可以将其拆分为单步排序
插入排序
单步插入排序是将需要排序的元素,按其排序码的大小,插入到已经有序的序列中
直接插入排序
当插入第 i (i >= 1)个元素时,前面 i - 1个元素已经有序,此时用array[i]的排序码和前面元素array[i - 1], array
[i - 2],…的排序码进行顺序比较,找到arrry[i]要插入的位置将其插入,原来位置上的元素后移。
从小到大排序比较时,如果当前元素排序码大于array[i]时当前元素后移,继续比较直到找到比其小的,将其放到正在比较元素的后边。 可见相等元素比较时,原来靠后的还是排在后边,所以插入排序是稳定的。元素集合越接近有序需要比较的次数越少元素移动相应越少, 排序算法的时间效率越高,相反当元素集合接近逆序时时间效率越低。
void InsretSort(int * array, size_t n)
{
assert(array);
for (size_t i = 1; i < n; ++i) //[1,n)
{
int end = i - 1; //[0,end]为有序区间
int cur = array[i];
while (end >= 0 && array[end] > cur)
{
array[end + 1] = array[end--];//元素后移
}
array[end + 1] = cur;
}
}
希尔排序
希尔排序时插入排序的一种改进,基本思想是将元素集合相距某个增量 d 的元素组成一个子序列,通过插入排序使其有序,然后逐渐减少增量 d 直到 d 为 1 进行一次快速排序达到整体有序。
希尔排序步长的选择是最重要的部分,会影响其效率,其设计者最初建议步长选择为n/2并且对步长取半直到步长达到1。
目前已知最好的步长序列是由Sedgwick提出的(1, 5, 19, 41, 109,…)。
void ShallSort(int * array, size_t n)
{
assert(array);
for (int gap = n >> 1; gap > 0; gap >>= 1) //步长选择为n/2并对步长取半达到1
{
for (size_t i = gap; i < n; i++)
{
int end = i - gap;
int cur = array[i];
while (end >= 0 && array[end] > cur)
{
array[end + gap] = array[end];
end -= gap;
}
array[end + gap] = cur;
}
}//gap
}
选择排序
直接选择排序
选择排序的思想是首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕
void _Swap(int *left, int *right) //交换两个数
{
int temp = *left;
*left = *right;
*right = temp;
}
//一次选择一个最小值
void SelectionSort(int array[], int len)
{
for (int i = 0 ; i < len - 1 ; i++)
{
int min = i;
for (int j = i + 1; j < len; j++) //遍历未排序的元素
{
if (array[j] < array[min])//找到最小的元素下标
{
min = j;
}
}
_Swap(&array[min], &array[i]); //进行值的交換
}
}
//每次选择一个最小的和一个最大的
void SelectSort(int * array, size_t n)
{
assert(array);
int min = 0, max = 0;
for (size_t left = 0, right = n - 1; left < right; left++, right--)
{
min = left;
max = right;
for (size_t i = left; i <= right; ++i) //遍历未排序的元素
{
if (array[i] < array[min]) //找到最小的元素下标
{
min = i;
}
if (array[i] > array[max]) //找最大的元素下标
{
max = i;
}
}
if (min != left) //最小的元素放在左边
{
_Swap(&array[min], &array[left]);
}
if (max == left) //如果最大的元素在最小元素位置,将max改为最小元素交换后的位置
{
max = min;
}
if (max != right) //将最大的元素放在右边
{
_Swap(&array[max], &array[right]);
}
}
}
堆排序
堆排序是利用堆的数据结构设计的排序算法, 大堆是节点的左右子树均小于自己,小堆相反。堆排其思想是把数组看成堆,排序的第一步是建堆,然后是取堆顶元素调整堆 ,建堆是通过自底向上执行向下调整,向下调整是父亲节点的左右子树满足大(小)堆,选出左右节点最大(小)的和父亲节点进行交换,之后用同样的逻辑调整被交换的子树直到叶子节点。
排序过程是:把堆顶array[0]元素和当前最堆的最后一个元素交换;堆元素个数减1;由于第1步后根节点不再满足最堆定义,向下调整根结点。直到剩一个元素排序结束。因为每次要和堆的最后一个元素交换因此堆排是一种不稳定的排序
static void AdjustDown(int * array, size_t n, int root) //向下调整
{
assert(array);
//在阵列起始位置为0的情形中:
//父节点i的左子节点在位置(2i+1);
//父节点i的右子节点在位置(2i+2);
//子节点i的父节点在位置 (i-1)/2);
int parents = root;
int child = parents * 2 + 1;
while (child < (int)n)
{
if (child + 1 < (int)n && array[child] < array[child + 1]) //找到左右孩子大的一个
{
child++;
}
if (array[child] > array[parents]) //和父亲节点进行比较如果父亲节点小进行交换继续调整其字树
{
_Swap(&array[parents], &array[child]);
parents = child;
child = parents * 2 + 1;
}
else //如果其满足大堆直接跳出
{
break;
}
}
}
void HeapSort(int * array, size_t n)
{
assert(array);
//建堆
for (int i = ((n - 2) >> 1); i >= 0; --i)
{
AdjustDown(array, n, i);
}
//进行排序
int end = n - 1;
while (end > 0)
{
_Swap(&array[end], &array[0]); //和最后一个进行交换
AdjustDown(array, end, 0); //重新调整成大堆
end--;
}
}
交换排序
冒泡排序
冒泡排序的步骤如下:
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3、针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
void BubbleSort(int * a, int n)
{
assert(a);
int end = n - 1;
while (end > 0) //控制冒泡结束的位置
{
int flag = 0; //设标志
for (int i = 0; i < end; ++i) //在区间 [0, end)
{
//比较相邻的元素。如果第一个比第二个大,就交换他们两个。
if (a[i] > a[i + 1])
{
_Swap(&a[i], &a[i+1]);
flag = 1; //如果交换标志就设为1
}
}
if (flag == 0) //如果没有交换就说明有序直接结束
{
break;
}
end--;
}
}
快速排序
快速排序使用分治法策略来把一个序列分为两个子序列。
步骤为:
从数列中挑出一个元素,称为”基准”,pivot
重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面在这个分割结束之后,该基准就处于数列的中间位置。
把数组分为 [left piviot - 1] [pivot + 1, right]两个区间
到最底部时,数列的大小是零或一,也就是已经排序好了
插入排序在小数组的排序上是非常高效的,在快速排序递归的子序列,如果序列规模足够小,可以使用插入排序替代快速排序,因此可以在快排之前判断数组大小,如果小于一个阀值就使用插入排序可以对快排进行优化
//三数取中
int GetMin(int x[], int a, int b, int c)
{
return x[a] < x[b] ? (x[b] < x[c] ? b : x[a] < x[c] ? c : a)
: x[b] > x[c] ? b : x[a] > x[c] ? c : a;
}
//左右指针法
int PartSort1(int * array, int left, int right)
{
assert(array);
//使用三数取中法规避数据一边倒
int mid = GetMin(array, left, left + ((right - left) >> 1), right);
_Swap(&array[mid], &array[right]);
int pivot = right;//选数组最右边值为枢纽值
while (left < right) //当左右相遇时结束交换枢纽值
{
//左边遇到比枢纽值小的和相等值往后走, 遇到大于枢纽值的的值停止
while (left < right && array[left] <= array[pivot])
{
left++;
}
//右边遇到比枢纽值大的和相等值往后走, 遇到小于枢纽值的的值停止
while (left < right && array[right] >= array[pivot])
{
right--;
}
//将左右值交换 使小于枢纽值的放在左边, 大于枢纽值的放在右边
_Swap(&array[left], &array[right]);
}
//找到枢纽值的位置并和枢纽值交换
_Swap(&array[left], &array[pivot]);
return left;
}
//挖坑法
int PartSort2(int * array, int left, int right)
{
assert(array);
//选数组最右边值为枢纽值,使用三数取中法规避数据一边倒
int mid = GetMin(array, left, left + ((right - left) >> 1), right);
_Swap(&array[mid], &array[right]);
//第一次最右面为坑
int pivot = array[right];
while (left < right)
{
while (left < right && array[left] <= pivot)
{
left++;
}
array[right] = array[left]; //将右边坑填上后左边为坑
while (left < right && array[right] >= pivot)
{
right--;
}
array[left] = array[right]; //将左边坑填上之后右边为坑
}
array[left] = pivot;//填上左右相遇点的坑
return left;
}
//前后指针法
int PartSort3(int * array, int left, int right)
{
assert(array);
//选数组最右边值为枢纽值,使用三数取中法规避数据一边倒
int mid = GetMin(array, left, left + ((right - left) >> 1), right);
_Swap(&array[mid], &array[right]);
int prev = left - 1, cur = left;
while (cur < right)
{
if (array[cur] <= array[right] && ++prev != cur )
{
_Swap(&array[prev], &array[cur]);
}
cur++;
}
_Swap(&array[right], &array[++prev]);
return prev;
}
//递归法
void QuickSort(int * a, int left, int right)
{
assert(a);
if (left >= right)
{
return;
}
//如果小于阀值7就使用插入排序
if (right - left + 1 == 7)
{
InsretSort(array, right - left + 1);
return;
}
//从数列中挑出一个元素,称为"基准",pivot
//重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面在这个分割结束之后,该基准就处于数列的中间位置。
int div = PartSort3(a, left, right);
QuickSort(a, left, div - 1);
QuickSort(a, div + 1, right);
}
//非递归
void QuickSortNR(int * array, int left, int right)
{
assert(array);
Stack S;
StackInit(&S, 10);
//先入左然后入右即取出时相反
StackPush(&S, left);
StackPush(&S, right);
while (!StackEmpty(&S))
{
//出栈时先出右再出左
int _right = StackTop(&S);
StackPop(&S);
int _left = StackTop(&S);
StackPop(&S);
//如果小于阀值7就使用插入排序
if (right - left + 1 == 7)
{
InsretSort(array, right - left + 1);
continue;
}
int div = PartSort1(array, _left, _right);
if (_left < div - 1)
{
StackPush(&S, _left);
StackPush(&S, div - 1);
}
if (div + 1 < _right)
{
StackPush(&S, div + 1);
StackPush(&S, _right);
}
}
}
归并排序
归并操作,也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。
递归法:
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针到达序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
迭代法:
原理如下假设序列共有 n 个元素:
将序列每相邻两个数字进行归并操作,形成 ceil(n/2) 个序列,排序后每个序列包含两个/一个元素若此时序列数不是1个则将上述序列再次归并,形成 ceil(n/4)}个序列,每个序列包含四个/三个元素,重复操作,直到所有元素排序完毕,即序列数为1。
void _MergeSort(int * array, int left, int right, int * tmp)
{
assert(array);
if (left >= right)
{
return;
}
int mid = left + ((right - left) >> 1);
//分为[left mid] [mid + 1 right]两个区间
_MergeSort(array, left, mid, tmp);
_MergeSort(array, mid + 1, right, tmp);
//至此左右区间均有序
int left_a = left, right_a = mid;
int left_b = mid + 1, right_b = right;
int index = left;
while (left_a <= right_a && left_b <= right_b)
{
//从小到大排序
if (array[left_a] > array[left_b])
{
tmp[index++] = array[left_b++];
}
else
{
tmp[index++] = array[left_a++];
}
}
while (left_a <= right_a)
{
tmp[index++] = array[left_a++];
}
while (left_b <= right_b)
{
tmp[index++] = array[left_b++];
}
memcpy(&array[left], &tmp[left], sizeof(int)* (right - left + 1));
}
void MergeSort(int * array, int left, int right)
{
assert(array);
int * tmp = (int *)malloc(sizeof(int) * (right - left + 1));
_MergeSort(array, left, right, tmp);
free(tmp);
}
//迭代法
void MergeSortNR(int * array, int left, int right)
{
assert(array);
int * tmp = (int *)malloc(sizeof(int) * (right - left + 1));
for (int gap = 1; gap <= (right - left + 1); gap <<= 1)
{
int cur = left;
while (cur <= right)
{
//计算区间中点分为两个区间 [cur mid - 1] [mid mid + gap - 1]
int mid = cur + gap;
int left_a = cur, right_a = mid - 1;
if (right_a >= right)
{
break;
}
int left_b = mid, right_b = left_b + gap - 1;
if (right_b > right)
{
right_b = right;
}
int index = cur;
while (left_a <= right_a && left_b <= right_b)
{
//从小到大排序
if (array[left_a] > array[left_b])
{
tmp[index++] = array[left_b++];
}
else
{
tmp[index++] = array[left_a++];
}
}
while (left_a <= right_a)
{
tmp[index++] = array[left_a++];
}
while (left_b <= right_b)
{
tmp[index++] = array[left_b++];
}
////计算需要拷贝的空间字节大小
int count_byte = (right_b - cur + 1) < (gap << 1) ? (right_b - cur + 1) : (gap << 1);
memcpy(&array[cur], &tmp[cur], sizeof(int)* count_byte);
cur += (gap << 1);
}//while(cur <= right)
}
free(tmp);
}