目录
本文围绕排序算法展开,对冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序以及堆排的时间复杂度,空间复杂度,代码以及代码思路做了详细概括,文章中可能出现些许错误,望指正。
1.冒泡排序
冒泡排序是一种简单的排序算法,其基本思想是通过重复遍历待排序的数列,比较相邻的元素,并将顺序错误的元素交换过来,从而把最大(或最小)的元素“冒泡”到数列的一端。
以下是冒泡排序的基本步骤:
- 从第一个元素开始,比较相邻的两个元素。
- 如果第一个元素大于第二个元素,则交换它们的位置。
- 对每一对相邻元素进行同样的操作,一直到最后一个元素。这一遍下来,最大的元素会被移动到数列的最后一个位置。
- 对剩下的元素重复以上步骤,直到没有需要交换的元素为止。
//冒泡排序
void bubbleSort(int* arr, int len) {
for (int i = 0; i < len - 1; i++) {
// 设置一个标志,判断是否需要提前结束排序
int swapped = 0;
for (int j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换相邻元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1; // 发生了交换
}
}
// 如果没有发生交换,说明数组已经有序,可以提前结束
if (!swapped) {
break;
}
}
}
时间复杂度为 O(n^2),空间复杂度为O(1),虽然冒泡排序简单易懂,但在实际中对于大规模数据的排序效率较低,通常不推荐使用。
2.选择排序
选择排序是一种简单直观的排序算法,其基本思想是通过不断选择最小(或最大)的元素,将其放到已排序序列的末尾,从而实现排序。
选择排序的主要步骤如下:
- 从待排序的数组中找出最小(或最大)元素,并将其与数组的第一个元素交换。
- 在剩下的未排序元素中继续寻找最小(或最大)元素,并将其与第二个元素交换。
- 重复以上过程,直到整个数组都有序。
//选择排序
void selectionSort(int* arr, int len) {
if (arr == NULL || len < 2) return;
for (int i = 0; i < len - 1; i++) {
// 假设当前元素是最小值
int minIndex = i;
for (int j = i + 1; j < len; j++) {
// 找到更小的元素
if (arr[j] < arr[minIndex]) {
minIndex = j; // 更新最小值索引
}
}
// 如果找到的最小值不是当前值,则交换
if (minIndex != i) {
swap(&arr[i], &arr[minIndex]);
}
}
}
时间复杂度为 O(n^2),空间复杂度是 O(1),其中 n 是数组的长度。虽然它的实现比较简单,但在实际应用中效率较低,因此对于大规模数据排序不太适用。
3.插入排序
插入排序是一种简单的排序算法,其基本思想是在一个已排序的序列中插入一个新的元素,从而得到新的有序序列。该算法通常用于小规模的数据排序。
插入排序的主要步骤如下:
-
从第二个元素开始,认为前面的元素是一个已排序的序列。
- 取出当前元素,与已排序序列中的元素进行比较,从后向前找到合适的位置插入。
- 将当前元素插入到正确的位置,并保持已排序序列的顺序。
- 重复以上步骤,直到所有元素都被插入到正确的位置。
//插入排序
void insertsort(int* arr, int len) {
if (arr == NULL || len == 1) return;
int j = 0;
for (int i = 1; i < len; i++) {
int temp = arr[i];
for ( j = i - 1; j >= 0; j--) {
if (arr[j] > temp) {
arr[j + 1] = arr[j];
}
else {
//arr[j + 1] = temp;
break;
}
}
arr[j + 1] = temp;
}
}
插入排序的时间复杂度为 O(n^2),空间复杂度为O(1),但对于基本有序的数组,它的性能会比较好,接近 O(n)。越有序越快
4.希尔排序
希尔排序是一种基于插入排序的算法,它通过将元素分组来实现更高效的排序。希尔排序的基本思想是将整个待排序的数组分成若干个子序列,对每个子序列进行插入排序,随着排序的进行,子序列的间隔逐渐减小,最后进行一次整体的插入排序。
上面的代码实现了希尔排序,具体的实现思路如下:
- 分组:依据给定的步长(
step
),将数组分成若干个子序列。多个元素之间间隔为step
。 - 排序:对每个子序列使用插入排序。具体步骤通过外层循环遍历从
step
开始到数组的末尾。 - 插入:在内部循环中,将待插入的元素(
temp
)与当前子序列的元素比较,如果子序列中的元素大于temp
,则将其向后移动,直到找到合适的位置为止。 - 逐步减小步长:随着
step
的减小,算法不断对规模增大的子序列进行插入排序,直到最后step
为 1,这时整个序列进行最后的插入排序。
//希尔排序
void shellsort(int* arr, int len,int step) {
if (arr == NULL || len == 1) return;
int j = 0;
for (int i = step; i < len; i++) {
int temp = arr[i];
for (j = i - step; j >= 0; j -= step) {
if (arr[j] > temp) {
arr[j + step] = arr[j];
}
else {
//arr[j + 1] = temp;
break;
}
}
arr[j + step] = temp;
}
}
5.归并排序
归并排序是一种有效的排序算法,采用分治法(Divide and Conquer)进行排序。其基本思想是将数组分成两个子数组,分别对这两个子数组进行排序,然后将排好序的子数组合并在一起组成最终的排序结果。
归并排序的主要步骤如下:
- 分解:将待排序的数组分成两半,分别对这两半进行递归调用归并排序。
- 解决:当数组的长度为 1 时,该数组已经是有序的,直接返回。
- 合并:将两个已排序的子数组合并成一个有序的数组。
//归并排序
#if 0
vector<int> arr2;
while (r2 < len) {
int i = 0;
for (l1; l1 <= r1 && l2 <= r2;) {
if (arr[l1] < arr[l2]) {
arr2[i] = arr[l1];
l1++;
i++;
}
else {
arr2[i] = arr[l2];
l2++;
i++;
}
}
if (l1 <= r1) {
for (l2; l2 <= r2; l2++) {
arr2[i] = arr[l2];
i++;
}
}
if (l2 <= r2) {
for (l1; l1 <= r1; l1++) {
arr2[i] = arr[l1];
i++;
}
}
l1 = r2 + gap;
r1 = l1 + gap;
l2 = r1 + 1;
r2 = l2 + gap;
if (r2 > len) r2 = len;
}
#endif
// gap 代表归并段存在几个数据
//arr 原始数据 tmp 辅助函数
static void merge(int* arr, int* tmp, int len, int gap) {
int l1 = 0;
int r1 = l1 + gap-1;
int l2 = r1 + 1;
int r2 = l2 + gap-1;
if (r2 >= len) r2 = len-1;
int i = 0;
while (l2 < len) {
//谁小谁下来
// 如果有两个归并段
while (l1 <= r1 && l2 <= r2) {
if (arr[l1] < arr[l2])
tmp[i++] = arr[l1++];
else
tmp[i++] = arr[l2++];
}
//第二个归并完成
while (l1 <= r1) {
tmp[i++] = arr[l1++];
}
//第一个归并完成
while (l2 <= r2) {
tmp[i++] = arr[l2++];
}
l1 = r2 + 1;
r1 = l1 + gap - 1;
l2 = r1 + 1;
r2 = l2 + gap - 1;
if (r2 >= len) r2 = len - 1;
}
//不足两个归并段
for (int j = l1; j < len - 1; j++) {
tmp[i++] = arr[j];
}
//将tmp导入arr
memcpy(arr, tmp, sizeof(tmp[0]) * len);
}
void mergesort(int* arr, int len) {
int* tmp = (int*)malloc(sizeof(int) * len);
for (int i = 1; i < len; i *= 2) {
//遍历次数 1*2*2*2*2==n log(2)n
merge(arr, tmp, len, i);
}
free(tmp);
}
归并排序的时间复杂度为 O(n log n),无论是在最坏情况下还是平均情况下,都是如此。它的空间复杂度为 O(n) 由于需要额外的存储空间来存放合并后的数组。
6.快速排序
快速排序是一种高效的排序算法,采用分治法(Divide and Conquer)策略。它的主要思想是通过一个“基准”元素将待排序的数组分成两个子数组,其中左侧的元素都小于等于基准元素,右侧的元素都大于基准元素,接着递归地对这两个子数组进行快速排序。
快速排序的主要步骤如下:
- 选择基准:从数组中选择一个基准元素(通常选择第一个元素、最后一个元素或中间的元素)。
- 分区:通过一趟遍历,将数组分成两部分:小于等于基准的元素和大于基准的元素。
- 递归排序:对左右两个子数组递归进行快速排序。
- 合并:递归的基础上,子数组自动合并成一个有序的数组。
//快速排序
// 待排序序列首元素,作为基准,一次分割,将左右位置元素划分,
//分割函数,返回值,基准存放下标
static int partition(int* arr, int len,int i, int j) {
int tmp = arr[i];
while (i < j) {
while (i<j && arr[j] > tmp)
j--;
arr[i] = arr[j];
while (i < j && arr[i] < tmp)
i++;
arr[j] = arr[i];
}
//当i==j
arr[i] = tmp;
return i;
}
void Quick(int* arr, int len, int i, int j) {
//一次分割之后,基准存放位置
int index = partition(arr, len, i, j);
if (index - i > 1) {
Quick(arr, len, i, index - 1);
}
if (j - index > 1) {
Quick(arr, len, index + 1, j);
}
}
void QuichSort(int* arr, int len) {
Quick(arr, len, 0, len - 1);
}
快速排序的平均时间复杂度为 O(n log n),最坏情况下为 O(n^2)(例如输入数组已经是有序的情况下),但通过随机选择基准或者三数取中法可以降低出现最坏情况的概率。其空间复杂度为 O(log n),主要消耗在递归栈上。
7.堆排
堆排序(Heap Sort)是一种基于比较的排序算法,利用堆数据结构实现排序。堆是一种特殊的完全二叉树,满足堆的性质:在最大堆中,父节点的值大于或等于其子节点的值;在最小堆中,父节点的值小于或等于其子节点的值。
堆排序的主要步骤如下:
- 构建初始堆:将待排序的数组构建成一个最大堆或最小堆。
- 交换:将堆的根节点(最大值或最小值)与最后一个元素交换,然后减少堆的大小。
- 调整堆:对新的堆进行调整,使其重新满足堆的性质。
- 重复:重复步骤 2 和步骤 3,直到堆的大小为 1。
//堆排
/*
1.将数组调整为大根堆,
从数组倒数第一个,非叶子节点开始,将堆顶元素,从下依次调整
堆顶元素 与 数组最后一个元素交换
*/
void swap(int* a, int* b) {
int t = *a;
*a = *b;
*b = t;
}
void adjust(int* arr, int begin, int end) {
int root = begin;
int i = root, j = 2 * i + 1, tmp = arr[root];
while (j <= end) {
if (arr[j] < arr[j + 1] && j + 1 <= end) {
j = j + 1; //j标记左右节点,较大值
}
if (arr[i] < tmp) break;
arr[i] = arr[j];
i = j;
j = 2 * i + 1;
}
}
void heapsort(int* arr, int len) {
//调整大根堆
for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
adjust(arr,i,len);
}
swap(&arr[0], &arr[len - 1]);
for (int i = 1; i < len - 1; i++) {
adjust(arr, 0, len - 1 - i);
swap(&arr[0], &arr[len - 1 - i]);
}
}
堆排序的时间复杂度为 O(n log n),并且是一个原地排序算法,空间复杂度为 O(1)。