1.排序
1.1什么是排序
排序是一种操作,通过比较记录中的关键字,将一组数据按照特定顺序(递增或递减)排列起来。排序在计算机科学中非常重要,因为它不仅有助于数据的快速检索,还能提高其他算法的性能。
1.2稳定性
排序算法的稳定性与排序后的记录相对顺序有关。稳定性的重要性在于,当关键字相同时,保持记录的原有顺序可能会影响后续操作。例如,在某些情况下,次要关键字可能仍然是有序的,这样的排序称为稳定排序。常见的稳定排序算法包括:
- 冒泡排序
- 插入排序
- 归并排序
- 计数排序
不稳定排序则不会保证相同关键字的记录顺序不变,例如:
- 快速排序
- 堆排序
- 选择排序
1.3内部排序
内部排序是指数据全部加载到内存中进行排序。这类排序算法通常在数据量较小或内存充足的情况下使用。常见的内部排序算法有:
- 快速排序
- 归并排序
- 堆排序
1.3外部排序
外部排序适用于数据量大到无法完全加载到内存中的情况,这时需要使用外存(如磁盘)来存储数据。常见的外部排序方法是多路归并排序,它通过将数据分成若干个小块分别排序,然后依次合并这些已排序的小块。
2.排序算法
2.1插入排序
2.1.1算法思想
插入排序就跟玩扑克牌,当插入新牌(元素)时,跟前面已经排好序的牌(元素)依次比较,找到插入的位置后插入,如果后面有牌(元素),原来位置上的牌(元素)后移。
2.1.2算法特性
- 时间复杂度:O(n^2)
插入第 2 个元素时,比较 1 次。
插入第 3 个元素时,比较 2 次。
.........
插入第i个元素时,比较 i−1 次。
我们在计算时间复杂度时,要考虑最坏的情况。
当我们对n个数进行插入排序的时候,会执行的总次数为:
1+2+3+⋯+(n−1) =n(n-1)/2
这是一个等差数列求和,当n较大的时候n^2占主导地位,时间复杂度为O(n^2);
- 空间复杂度:O(1),它是一种稳定的排序算法
2.1.3 插入排序代码实现
// 时间复杂度:O(N^2) -- 逆序(从后向前比较插入)
// 最好 O(N) -- 顺序有序 或 接近顺序有序
void InsertSort(int* a, int n) //从小到大排序
{
for (int i = 0; i < n - 1; ++i)
{
int end = i;
// 单趟排序:[0, end]有序 end+1位置的值,插进,保持他依旧有序
int tmp = a[end + 1]; //新插入的元素
while (end >= 0)
{
if (tmp < a[end]) //如果新插入的数据小于最后的
{
a[end + 1] = a[end]; //将原始最后的数据向后移动,往前找,直到到最前面。
--end;
}
else
{
break;
}
}
a[end + 1] = tmp; //如果新插入的数据大于最后的,将这个数据放到最后面
}
}
2.2希尔插入排序( 缩小增量排序 )
希尔排序(Shell Sort),也称为缩小增量排序,是插入排序的一种改进版本。它通过比较和交换不相邻的元素,逐步减少数据的无序程度,最终通过标准的插入排序完成排序。希尔排序的设计目的是为了改进插入排序在处理大规模无序数据时的性能。
2.2.1算法思想
希尔排序通过将数组按一定的间隔(称为增量)分成若干组,每组内分别进行直接插入排序。随着排序的进行,逐步缩小间隔,直到间隔为 1 时,整个数组进行最后一次插入排序。这时的数组已经接近有序,因此插入排序的效率会非常高。
操作步骤
- 选择增量序列:选定一个增量序列 d1,d2,…,dk,其中 dk 必须为 1。常见的增量序列有 。
- 分组排序:对于每一个增量 di,将待排序数组分成若干个间隔为 di的子序列,对每个子序列进行插入排序。
- 缩小增量:逐步缩小增量,重复上述分组排序的过程,直到增量为 1。
2.2.2算法特性
- 时间复杂度:O(n^1.3~n^2)
增量序列的选择对希尔排序的时间复杂度有着决定性的影响。
不同的增量序列会导致不同的比较和交换次数,从而影响算法的整体效率。
希尔增量序列
每一个增量序列的长度为
对每个子序列执行插入排序的时间复杂度大约为:
由于有 gap 个子序列,因此在增量为 gapgapgap 时,总的时间复杂度为:
总时间复杂度是所有增量下的时间复杂度之和:
这个和式是一个等比数列,前 log2n项的和可以近似为:
因此,总的时间复杂度可以近似为:
- 空间复杂度:O(1),它是一种稳定的排序算法
2.2.3希尔排序代码实现
void ShellSort(int* a, int n)
{
// 1、gap > 1 预排序
// 2、gap == 1 直接插入排序
int gap = n;
while (gap > 1)
{
//gap = gap/2;
gap = gap / 3 + 1;
//***************这部分就是插入排序************start
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
//***************这部分就是插入排序************end
}
}
2.3直接选择排序
2.3.1算法思想
直接选择排序(Selection Sort)是一种简单直观的排序算法。它的基本思想是每一趟从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
直接选择排序的基本步骤
-
初始未排序区间:从数组的第一个元素开始,逐步减少未排序区间的长度。
-
找到最小值:在未排序区间中找到最小值(或最大值)。
-
交换位置:将最小值(或最大值)与未排序区间的第一个元素交换。
-
缩小未排序区间:重复上述步骤,直到未排序区间只剩下一个元素,此时数组已排序。
2.3.2算法特性
-
时间复杂度:直接选择排序的时间复杂度为O(n^2)。具体分析如下:
外层循环运行 n−1次。
循环在第一次执行时运行 n−1次,第二次执行时运行 n−2,依此类推,总共进行比较的次数为 n+(n-1)+...+1次。因此,总的时间复杂度为 O(n^2)。
-
空间复杂度:直接选择排序是一种原地排序算法,它只需要常数级别的辅助空间,因此空间复杂度为 O(1)。
-
稳定性:直接选择排序是一种不稳定的排序算法。例如,当数组中有两个相同的元素时,可能会在排序过程中改变它们的相对位置。
2.3.3算法代码实现
void SelectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// 假设当前 i 位置上的元素为未排序区间的最小值
int minIndex = i;
// 在未排序区间中找到最小值的索引
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 将未排序区间的最小值与当前 i 位置的元素交换
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
//改进的选择排序,通过在每次迭代中同时选择最小值和最大值并将它们放在各自的正确位置来减少排序的轮数。
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void SelectSort(int* a, int n)
{
int left = 0, right = n- 1;
while (left < right)
{
int mini = left, maxi = left;
for (int i = left+1; i <= right; ++i)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[left], &a[mini]);
// 如果left和maxi重叠,修正一下maxi即可
if (left == maxi)
maxi = mini;
Swap(&a[right], &a[maxi]);
left++;
right--;
}
}
2.4堆选择排序
堆排序(Heap Sort)是一种利用堆这种数据结构进行排序的算法。堆是一种特殊的完全二叉树,有最大堆和最小堆两种类型。堆排序的核心思想是利用堆的性质来高效地进行排序。
堆的定义
- 最大堆(Max-Heap):在最大堆中,任意节点的值都不小于其子节点的值,因此根节点的值是最大值。
- 最小堆(Min-Heap):在最小堆中,任意节点的值都不大于其子节点的值,因此根节点的值是最小值。
2.4.1算法思想
排升序建大堆,排降序建小堆。
堆排序的基本步骤包括两个主要阶段:(以大堆为例)
- 构建初始堆:将无序数组构建成一个最大堆。
- 排序过程:
- 将最大堆的根节点(即最大值)与堆的最后一个元素交换,将最大值移到已排序区域。
- 调整堆,使剩余部分重新满足最大堆的性质。
- 重复上述步骤,直到堆为空。
2.4.2算法特性
- 时间复杂度
构建最大堆:将无序数组转化为最大堆的时间复杂度为 O(n)。
堆调整:每次从根节点提取最大值并重新调整堆的时间复杂度为 O(logn)。在整个排序过
程中需要执行 n次,因此总时间复杂度为 O(nlogn)。
- 空间复杂度:堆排序是原地排序算法,不需要额外的空间进行排序,空间复杂度为 O(1)。
- 稳定性:堆排序是一种不稳定的排序算法,因为在调整堆的过程中,可能会改变相同元素的相对位置。
2.4.3算法代码实现
void AdjustDown(int* a, size_t size, size_t root)
{
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
// 1、选出左右孩子中小的那个
if (child + 1 < size && a[child + 1] > a[child])
{
++child;
}
// 2、如果孩子小于父亲,则交换,并继续往下调整
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
// 向下调整--建堆 O(N)
for (int i = (n -1) /2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
size_t end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
2.5冒泡交换排序
2.5.1算法思想
冒泡排序的基本思想是:
- 依次比较相邻的两个元素,如果它们的顺序错误(即前一个比后一个大),则交换它们。
- 每一轮操作后,当前轮次中最大的元素会“冒泡”到列表的最后一个位置。
- 随着排序的进行,每轮操作后不需要比较的元素数量逐渐减少。
2.5.2算法特性
- 时间复杂度
时间复杂度为 O(n^2)。
- 空间复杂度:冒泡排序是原地排序算法,不需要额外的空间,空间复杂度为 O(1)。
冒泡排序是稳定的排序算法,因为在元素相等时,它不会改变相等元素的相对位置。
2.5.3算法代码实现
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//这里进行改进,如果一轮循环都没有交换位置,则说明已经排好顺序了
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; ++j)
{
int exchange = 0;
// 单趟
for (int i = 1; i < n-j; ++i) //每次找到一个最大的
{
if (a[i - 1] > a[i])
{
exchange = 1;
Swap(&a[i - 1], &a[i]);
}
}
if (exchange == 0)
{
break;
}
}
}
2.6快速交换排序(快排)
快速排序(Quick Sort)是一种高效的排序算法,采用了分治法的策略。它通过选择一个基准元素(pivot),将数组分成两部分:一部分包含所有小于基准元素的值,另一部分包含所有大于基准元素的值,然后递归地对这两部分进行排序。快速排序的平均时间复杂度为 O(nlogn),是许多实际应用中使用的排序算法。
2.6.1算法思想
- 选择基准元素(Pivot):从待排序数组中选择一个元素作为基准元素。
- 分区(Partitioning):重新排列数组,使得所有小于基准元素的值都在基准元素的左边,所有大于基准元素的值都在基准元素的右边。基准元素最终会被放到其正确的位置。
- 递归排序:递归地对基准元素左边和右边的子数组进行排序。
2.6.2算法特性
- 时间复杂度: O(nlogn)。这是因为每次分区操作将数组大致分成两部分,递归深度为 logn,每层的分区操作耗时为 O(n)。
- 空间复杂度: O(logn)。这是因为递归调用栈的最大深度为 logn,在空间上比起归并排序的 O(n)要更高效。
- 稳定性:快速排序是不稳定的排序算法,因为在排序过程中相同元素的相对位置可能会被改变。
2.6.3算法代码实现
分区算法
1.找中间数
int GetMidIndex(int* a, int left, int right)
{
//int mid = (left + right) / 2;
int mid = left + (right - left) / 2; //找到left和right中间的位置
// left mid right
if (a[left] < a[mid]) //找出 a[left] a[mid] a[right] 中的中间数的下标
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
2. Hoare 法
// hoare
int PartSort1(int* a, int left, int right)
{
int keyi = left; //先假设一个可以的数作为中间数
while (left < right) //让左边都是比a[keyi]小的数,让右边都是比a[keyi]大的数
{
// 找小
while (left < right && a[right] >= a[keyi]) //从右往左开始把比a[keyi]小的数找到
--right;
// 找大
while (left < right && a[left] <= a[keyi]) //从左往右开始把比a[keyi]大的数找到
++left;
Swap(&a[left], &a[right]); //进行交换
}
Swap(&a[keyi], &a[left]);
return left;
}
3.挖坑法
// 挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
// 坑位
int pit = left;
while (left < right)
{
// 右边先走,找小
while (left < right && a[right] >= key)
{
--right;
}
a[pit] = a[right];
pit = right;
// 左边走,找大
while (left < right && a[left] <= key)
{
++left;
}
a[pit] = a[left];
pit = left;
}
a[pit] = key;
return pit;
}
4.前后指针法
// 前后指针法
int PartSort3(int* a, int left, int right)
{
//int midi = GetMidIndex(a, left, right);
//Swap(&a[midi], &a[left]);
int keyi = left;
int prev = left, cur = left+1;
while (cur <= right)
{
if (a[cur] < a[keyi] && a[++prev] != a[cur])
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
5.递归排序
// O(N*logN)
int PartSort3(int* a, int left, int right)
{
//int midi = GetMidIndex(a, left, right);
//Swap(&a[midi], &a[left]);
int keyi = left;
int prev = left, cur = left+1;
while (cur <= right)
{
if (a[cur] < a[keyi] && a[++prev] != a[cur])
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort1(int* a, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1]keyi[keyi+1, end]
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi+1, end);
}
6.改进版排序
void QuickSort2(int* a, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
// 小区间直接插入排序控制有序
if (end - begin + 1 <= 10)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1]keyi[keyi+1, end]
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
}
7.利用栈创建非递归排序
// 非递归
void QuickSort3(int* a, int begin, int end)
{
ST st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, left, right);
// [left,keyi-1][keyi+1,right]
if (left < keyi-1)
{
StackPush(&st, left);
StackPush(&st, keyi-1);
}
if (keyi + 1 < right)
{
StackPush(&st, keyi+1);
StackPush(&st, right);
}
}
StackDestory(&st);
}
2.7归并排序
归并排序(Merge Sort)是一种基于分治法的排序算法,其核心思想是将一个大问题分解成多个小问题,分别解决这些小问题,再将它们合并成一个大问题的解决方案。归并排序是一种稳定的排序算法,其时间复杂度为 O(nlogn)。
2.7.1算法思想
- 分解(Divide):将待排序数组分为两个大致相等的子数组。
- 递归排序(Conquer):对这两个子数组递归地应用归并排序。
- 合并(Merge):将两个已排序的子数组合并成一个排序好的数组。
2.7.2算法特性
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)O(n)O(n)
归并排序需要额外的空间来存储临时数组,因此其空间复杂度为 O(n)。
- 稳定性:归并排序是稳定的排序算法。稳定性意味着在排序过程中,相等的元素保持它们原有的相对顺序。
2.7.3算法代码实现
递归实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin, mid][mid+1, end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
// 归并[begin, mid][mid+1, end]
//printf("归并[%d,%d][%d,%d]\n", begin, mid, mid+1, end);
int begin1 = begin, end1 = mid;
int begin2 = mid+1, end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a+begin, tmp+begin, (end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
assert(tmp);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
int gap = 1;
while (gap < n)
{
// 间距为gap是一组,两两归并
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
// end1 越界,修正
if (end1 >= n)
end1 = n - 1;
// begin2 越界,第二个区间不存在
if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
// begin2 ok, end2越界,修正end2即可
if (begin2 < n && end2 >= n)
end2 = n - 1;
// 条件断点
if (begin1 == 8 && end1 == 9 && begin2 == 9 && end2 == 9)
{
int x = 0;
}
printf("归并[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
int index = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
}
memcpy(a, tmp, n*sizeof(int));
//PrintArray(a, n);
gap *= 2;
}
free(tmp);
}