目录
一、排序
1.1 排序的概念
在数据结构中,排序是指将一组数据按照一定的规则或者条件进行重新排列的过程,以使得数据按照升序或降序排列。
排序是算法领域中非常重要的一个基本操作,常用于对数据进行整理、查找和优化处理。经常在日常生活中使用:
1.2 排序的要素:
- 数据集合:排序操作需要对一组数据进行排序,这些数据可以是整型、浮点型、字符型或其他自定义的数据类型。
- 排序规则:排序规则定义了将数据按照什么方式进行排序,常见的排序规则包括升序(从小到大)和降序(从大到小)。
- 排序算法:排序算法是一种具体的计算方法,用于按照排序规则对数据集合进行排序。常见的排序算法包括选择排序、插入排序、冒泡排序、快速排序、归并排序、堆排序等。
- 排序过程:排序过程是指根据选择的排序算法,逐步调整数据的位置,使其按照排序规则逐步有序的过程。
在C语言中,可以根据不同的排序需求选择合适的排序算法,并使用循环、条件语句等控制结构来实现排序过程。
二、排序算法
上面讲到常见的排序算法包括选择排序、插入排序、冒泡排序、快速排序、归并排序、堆排序等。此处我们着重讲解选择排序、插入排序、快速排序和归并排序。
2.1 选择排序
选择排序是一种简单直观的排序算法。它每次从待排序的元素中选择最小(或最大)的元素,放到已排序序列的末尾。
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,放到已排序序列的末尾,直到全部待排序的数据元素排完 。
//选择排序
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* arr, int len)
{
for (int end = 0; end < len; end++)
{
int next = end + 1;
int min = arr[end];
int minsub = end;
while (next < len)
{
if (arr[next] < min)
{
min = arr[next];
minsub = next;
}
next++;
}
Swap(&arr[end], &arr[minsub]);
}
}
2.2 插入排序
插入排序是一种简单直观的排序算法。
基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,通过逐步构建有序序列,最终完成排序。
步骤:
从第二个元素开始,将其与前面的已排序序列逐个比较,找到合适的位置插入。重复这个过程,直到所有元素都有序。
//插入排序
void InsertSort(int* arr, int len)
{
for (int i = 1; i < len; i++)
{
//一次排序
int end = i - 1;
int tmp = arr[i];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end --;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
2.3 插入排序的进阶:希尔排序
希尔排序又称缩小增量法,是插入排序的一种改进算法。
基本思想:
它通过将待排序序列划分成若干个子序列,分别对每个子序列进行插入排序,然后逐步缩小增量,最后再对整个序列进行一次插入排序。
步骤:
- 首先选取一个增量gap(也称为间隔)值,通常为原序列长度的一半(或三分之一+1),然后按照该增量将原序列分割成若干个子序列。
- 对每个子序列进行插入排序。
- 不断缩小增量(gap /= 2或gap = gap/3 + 1),重复步骤2,直到增量减小为1。
- 最后进行一次增量为1的插入排序,完成排序过程。
//希尔排序
void ShellSort(int* arr, int len)
{
int gap = len;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < len - gap; i++)
{
//一次排序
int end = i;
int tmp = arr[i + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
2.4 快速排序
2.4.1 Hoare法
Hoare于1962年提出的一种二叉树结构的交换排序方法。
基本思想:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
步骤如下:
选择基准元素(key):从待排序序列中选择一个元素作为基准元素,通常选择第一个元素、最后一个元素或者随机选择一个元素作为基准。
划分操作:定义两个指针,一个指向序列的起始位置(左指针),一个指向序列的结束位置(右指针)。然后从右指针开始向左移动,直到找到一个比基准元素小的元素;再从左指针开始向右移动,直到找到一个比基准元素大的元素。如果左指针位置在右指针位置的左侧(即还没有相遇),则交换左右指针所指向的元素。重复这个过程,直到左指针和右指针相遇。
分治递归:将序列划分为两个子序列(左边子序列和右边子序列)。对左边子序列和右边子序列分别递归调用快速排序算法,直到每个子序列只剩下一个元素或为空。
合并结果:递归的基本情况是子序列只有一个元素,此时已经有序。最后,将所有的子序列合并成一个有序序列。
之后是R向左移动,到达3的位置,L向右移动,与R相遇,基准值与相遇位置元素交换
左边做key,右边先走; 保障了相遇位置的值比key小 or 就是key的位置。
因为L和R相遇无非就是两种情况:L遇R和R遇L。
- 情况一: L遇R,R是停下来,L在走,R先走,R停下来的位置一定比key小相遇的位置就是R停下的位置,就一定比key要小
- 情况二: R遇L,在相遇这一轮,L没动,R在移动,跟L相遇,相遇位置就是L的位置,L的位置就是key的位置 or 交换过一些轮次,相遇L位置一定比key小 or 就是key的位置。
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//Hoare法
//单趟
int partSort(int* arr,int start, int end)
{
int key = arr[start];
int keyi = start;
while (start < end)
{
//从右往左找比key小的,从左往右找比key大的
while (start < end && arr[end] >= key)
{
end--;
}
while (start < end && arr[start] <= key)
{
start++;
}
Swap(&arr[start], &arr[end]);
}
Swap(&arr[keyi], &arr[start]);
return left;
}
void QuickSort(int* arr, int start, int end)
{
if (start >= end)
{
return;
}
int tmpi = partSort(arr, start, end);
QuickSort(arr, start, tmpi - 1);
QuickSort(arr, tmpi + 1, end);
}
2.4.2 挖坑法
基本思想:
- 挖坑法通过选择基准元素,将序列划分为两个部分,并找到一个位置作为空位。然后左右指针分别从两端向中间移动,将大于基准元素的值填入空位,直到左右指针相遇。最后,将基准元素放入空位,完成划分。
步骤:
- 选择基准元素(key),定义坑的下标(hole)。
- 定义左指针(left)和右指针(right),分别指向序列的起始位置和结束位置。
- 移动右指针,将小于基准元素的值填入空位,下标赋值给hole。
- 移动左指针,将大于基准元素的值填入空位,下标赋值给hole。
- 重复3、4,直到左右指针相遇。
- 将key的值赋给 hole对应的位置。
- 递归地对基准元素左边的子序列和右边的子序列进行挖坑法快速排序。
#include <stdio.h>
#include <stdlib.h>
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//挖坑法
//单趟
int partSort2(int* arr, int start, int end)
{
int key = arr[start];
int hole = start;
while (start < end)
{
//从右往左找比key小的,从左往右找比key大的
while (start < end && arr[end] >= key)
{
end--;
}
arr[hole] = arr[end];
hole = end;
while (start < end && arr[start] <= key)
{
start++;
}
arr[hole] = arr[start];
hole = end;
}
arr[hole] = key;
return hole;
}
void QuickSort(int* arr, int start, int end)
{
if (start >= end)
{
return;
}
int tmpi = partSort2(arr, start, end);
QuickSort(arr, start, tmpi - 1);
QuickSort(arr, tmpi + 1, end);
}
2.4.3 前后指针法
步骤:
- 选择基准元素(key)。
- 初始时,prev指针指向序列开头cur指针指向prev指针的后一个位置。
- 然后判断cur指针指向的数据是否小于key。若小于,则prev指针后移一位,并且cur指向的内容与prev指向的内容交换。cur指针++。
- 重复3,直至遍历完序列。(cur指针指向的数据若小于key,步骤相同,若大于key则cur指针继续++)
- 遍历完序列(cur指针越界),将prev指向的内容与key进行交换
- 结束,此时key左边的数据都比key小, key右边的数据都比key大
- 递归地对key左边的子序列和右边的子序列进行前后指针法快速排序。
//前后指针法
//单趟
int partSort3(int* arr, int start, int end)
{
int keyi = start;
int prev = start;
int cur = prev + 1;
while (cur <= end)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
void QuickSort3(int* arr, int start, int end)
{
if (start >= end)
{
return;
}
int tmpi = partSort3(arr, start, end);
QuickSort3(arr, start, tmpi - 1);
QuickSort3(arr, tmpi + 1, end);
}
2.4.4 快速排序优化
快速排序可以使用一些优化方法来提高性能,其中包括三数取中法选取基准元素和随机数法选择基准元素。
三数取中法选取基准元素:
基本思想:选择子数组的首、中、尾三个位置的元素,然后取它们的中间值作为基准元素。
- 优点:通过选择具有代表性的中间值作为基准元素,可以避免最坏情况的发生,即子数组分割非常不平衡的情况。
- 实现方法:比较数组首、中、尾三个位置的元素,找出中间值并将其作为基准元素。
随机数法选择基准元素:
- 基本思想:在每个子数组中随机选择一个元素作为基准元素。
- 优点:通过随机选择基准元素,可以降低最坏情况发生的概率,提高算法在各种输入情况下的性能。
- 实现方法:在每次递归调用快速排序时,随机生成一个索引,然后将对应的元素作为基准元素。
使用三数取中法后:
int GetMidIndex(int* arr, int left, int right)
{
int mid = (left + right) / 2;
if (arr[left] > arr[right])
{
if (arr[right] > arr[mid])
{
return right;
}
else if (arr[mid] > arr[left])//arr[mid] > arr[right]
{
return left;
}
else
{
return mid;
}
}
else//arr[left] < arr[right]
{
if (arr[right] < arr[mid])
{
return right;
}
else if (arr[left] < arr[mid])//arr[mid] < arr[right]
{
return mid;
}
else
{
return left;
}
}
}
//Hoare法
//单趟
int PartSort1(int* arr,int left, int right)
{
int midi = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[midi]);
int key = arr[left];
int keyi = left;
while (left < right)
{
//从右往左找比key小的,从左往右找比key大的
while (left < right && arr[right] >= key)
{
right--;
}
while (left < right && arr[left] <= key)
{
left++;
}
Swap(&arr[left], &arr[right]);
}
Swap(&arr[keyi], &arr[left]);
return left;
}
void QuickSort1(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int tmpi = PartSort1(arr, left, right);
QuickSort1(arr, left, tmpi - 1);
QuickSort1(arr, tmpi + 1, right);
}
//挖坑法
//单趟
int PartSort2(int* arr, int left, int right)
{
int midi = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[midi]);
int key = arr[left];
int hole = left;
while (left < right)
{
//从右往左找比key小的,从左往右找比key大的
while (left < right && arr[right] >= key)
{
right--;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] <= key)
{
left++;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
void QuickSort2(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int tmpi = PartSort2(arr, left, right);
QuickSort2(arr, left, tmpi - 1);
QuickSort2(arr, tmpi + 1, right);
}
//前后指针法
//单趟
int PartSort3(int* arr, int left, int right)
{
int midi = GetMidIndex(arr, left, right);
Swap(&arr[left], &arr[midi]);
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[cur], &arr[prev]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
void QuickSort3(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int tmpi = PartSort3(arr, left, right);
QuickSort3(arr, left, tmpi - 1);
QuickSort3(arr, tmpi + 1, right);
}
2.4.5 非递归的快排
非递归的快速排序是一种采用循环而不是递归来实现的快速排序算法。它使用一个栈来模拟递归过程,将待处理的子数组范围存储在栈中。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//非递归的快速排序
//使用栈
typedef struct Stack
{
int* a;
int size;
int capacity;
}st;
//初始化栈
void InitStack(st *pst)
{
int* tmp = (int*)malloc(sizeof(int) * 100);
if (tmp == NULL)
{
perror("malloc error!");
}
pst->a = tmp;
pst->capacity = 100;
pst->size = 0;
}
//入栈
void PushStack(st* pst, int x)
{
assert(pst);
assert(pst->a);
if (pst->size == pst->capacity)
{
int* tmp = (int*)realloc(pst, sizeof(int) * pst->capacity * 2);
if (tmp == NULL)
{
perror("realloc error!");
}
pst->a = tmp;
pst->capacity = pst->capacity * 2;
}
pst->a[pst->size++] = x;
}
//出栈
bool EmptyStack(st* pst)
{
assert(pst);
return pst->size == 0;
}
void PopStack(st* pst)
{
assert(pst);
assert(!EmptyStack(pst));
pst->size--;
}
//栈顶元素
int TopStack(st* pst)
{
assert(pst);
assert(!EmptyStack(pst));
return pst->a[pst->size - 1];
}
//销毁栈
void DestoryStack(st* pst)
{
assert(pst);
assert(pst->a);
free(pst->a);
free(pst);
}
//交换元素
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//单趟
int PartSort(int* arr, int left, int right)
{
assert(arr);
//使用挖坑法
int hole = left;
int key = arr[left];
while (left < right)
{
while (left < right && arr[right] >= key)
{
right--;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] <= key)
{
left++;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;
return hole;
}
void QuickSort(int* arr, int left, int right)
{
st* pst = (st*)malloc(sizeof(st));
InitStack(pst);
//先进后出
PushStack(pst, right);
PushStack(pst, left);
while (!EmptyStack(pst))
{
int tmpL = TopStack(pst);
PopStack(pst);
int tmpR = TopStack(pst);
PopStack(pst);
int keyi = PartSort(arr, tmpL, tmpR);
if (tmpR > keyi + 1)
{
PushStack(pst, tmpR);
PushStack(pst, keyi + 1);
}
if (tmpL < keyi -1)
{
PushStack(pst, keyi - 1);
PushStack(pst, tmpL);
}
}
}
void Print(int* arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 16,8,43,10,40,6,41,2 };
int len = sizeof(arr) / sizeof(arr[0]);
printf("非递归的快排\n");
Print(arr, len);
QuickSort(arr, 0, len - 1);
Print(arr, len);
return 0;
}
2.5 归并排序
2.5.1 递归法
归并排序是一种经典的排序算法,它基于分治策略,将待排序的数组分割成较小的子数组,然后逐步将这些子数组合并,最终得到一个有序的数组。
实现过程如下:
- 分割:将待排序数组从中间位置分割为两个子数组,再对这两个子数组递归地应用归并排序。
- 合并:将两个已排序的子数组合并成一个有序的数组。合并过程中,比较两个子数组的元素,将较小(或较大)的元素放入暂存数组,并移动相应指针,直到其中一个子数组的所有元素都放入暂存数组。
- 将另一个子数组直接拷贝到暂存数组中。
- 将排好序的暂存数组的数据拷贝到原数组。
//归并排序
void _MergeSort(int* arr, int left, int right, int* tmp)
{
if (left == right)
{
return;
}
int mid = (left + right) / 2;
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
memcpy(arr + left, tmp + left, sizeof(int)*(right - left + 1));
}
void MergeSort(int* arr, int len)
{
int* tmp = (int*)malloc(sizeof(int) * len);
_MergeSort(arr, 0, len - 1, tmp);
free(tmp);
}
2.5.2 归并排序的优化
归并过程中最后几层数据的比较和移动操作占用了归并排序的大部分时间,在一些特定情况下可以进行优化,减少最后几层数据操作时间,提高算法的效率。
一种针对归并排序的优化方法是使用直接插入算法来排序较小规模的子数组。当待排序的子数组大小较小时,直接插入算法比归并排序更加高效。
因此,可以通过设置一个阈值,在归并排序中,当子数组大小小于该阈值时,采用直接插入算法进行排序,这样可以减少递归调用的开销,并且直接插入算法在小规模数据中有较好的性能。
//归并排序的优化
void InsertSort(int* arr, int len)
{
for (int i = 1; i < len; i++)
{
int tmp = arr[i];
int j = i;
while (j >= 1)
{
if (arr[j - 1] > tmp)
{
arr[j] = arr[j - 1];
j--;
}
else
{
break;
}
}
arr[j] = tmp;
}
}
void _MergeSort(int* arr, int left, int right, int* tmp)
{
//阈值设置为3(少于4个元素就使用快排)
if (right - left <= 3)
{
InsertSort(arr + left, right - left + 1);
return;
}
int mid = (left + right) / 2;
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
memcpy(arr + left, tmp + left, sizeof(int) * (right - left + 1));
}
void MergeSort(int* arr, int len)
{
int* tmp = (int*)malloc(sizeof(int) * len);
_MergeSort(arr, 0, len - 1, tmp);
free(tmp);
}
2.5.3 非递归的归并排序
非递归的归并排序是一种利用迭代而非递归的方式实现归并排序的方法。它通过使用循环和迭代,而不是递归调用,来完成归并排序的过程。
//非递归的归并排序
void MergeSortNon_R(int* arr, int len)
{
int* tmp = (int*)malloc(sizeof(int) * len);
int gap = 1;
while (gap < len)
{
int tmpi = 0;
for (int i = 0; i < len; i = i + 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (end1 >= len || begin1 >= len)
{
break;
}
if (end2 >= len)
{
end2 = len - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[tmpi++] = arr[begin1++];
}
else
{
tmp[tmpi++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[tmpi++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[tmpi++] = arr[begin2++];
}
memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
}