🍕博客主页:️自信不孤单
🍬文章专栏:数据结构与算法
🍚代码仓库:破浪晓梦
🍭欢迎关注:欢迎大家点赞收藏+关注
文章目录
- 🍓冒泡排序
- 概念
- 算法步骤
- 动图演示
- 代码
- 🍊选择排序
- 概念
- 算法步骤
- 动图演示
- 代码
- 🍉插入排序
- 概念
- 算法步骤
- 动图演示
- 代码
- ❣️希尔排序
- 概念
- 算法步骤
- 动图演示
- 代码
- 🍥堆排序
- 概念
- 算法步骤
- 动图演示
- 代码
- 🍚快速排序
- 概念
- 算法步骤
- 动图演示
- 代码(递归)
- 代码(非递归)
- 🍕归并排序
- 概念
- 算法步骤
- 动图演示
- 代码
- 代码(非递归)
- 🍭计数排序
- 概念
- 算法步骤
- 动图演示
- 代码
- 🍧排序算法复杂度及稳定性
🍓冒泡排序
概念
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
算法步骤
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
动图演示
代码
void bubble_sort(int arr[], int len)
{
int i, j, temp, flag;
for (i = 0; i < len - 1; i++)
{
flag = 1;
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1])
{
flag = 0;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
if (flag)
return;
}
}
🍊选择排序
概念
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
算法步骤
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
动图演示
代码
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void selection_sort(int arr[], int len)
{
int i, j;
for (i = 0; i < len - 1; i++)
{
int min = i;
for (j = i + 1; j < len; j++)
if (arr[j] < arr[min])
min = j;
swap(&arr[min], &arr[i]);
}
}
🍉插入排序
概念
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
动图演示
代码
void insertion_sort(int arr[], int len)
{
int i, j, key;
for (i = 1; i < len; i++) {
key = arr[i];
j = i - 1;
while ((j >= 0) && (arr[j] > key))
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
❣️希尔排序
概念
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
算法步骤
选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
动图演示
代码
void shell_sort(int arr[], int len)
{
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap >>= 1)
for (i = gap; i < len; i++)
{
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
arr[j + gap] = arr[j];
arr[j + gap] = temp;
}
}
🍥堆排序
概念
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
算法步骤
- 创建一个堆;
- 把堆首(最大值)和堆尾互换;
- 把堆的大小缩小 1,并调用向下调整函数,目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2、3,直到堆的大小为 1。
动图演示
代码
void swap(int* a, int* b)
{
int temp = *b;
*b = *a;
*a = temp;
}
void adjust_down(int* a, int start, int end)
{
int parent = start;
int child = parent * 2 + 1;
while (child <= end)
{
if (child + 1 <= end && a[child] < a[child + 1])
child++;
if (a[parent] < a[child])
{
swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
return;
}
}
void heap_sort(int a[], int len)
{
for (int i = (len - 1 - 1) / 2; i >= 0; i--)
adjust_down(a, i, len - 1);
for (int i = len - 1; i > 0; i--)
{
swap(&a[0], &a[i]);
adjust_down(a, 0, i - 1);
}
}
🍚快速排序
概念
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
动图演示
代码(递归)
void swap(int* a, int* b)
{
int temp = *b;
*b = *a;
*a = temp;
}
// 三数取中
int get_mid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] > a[mid])
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
else
{
if (a[left] > a[right])
return left;
else if (a[mid] < a[right])
return mid;
else
return right;
}
}
// 快速排序hoare法
int part_sort1(int* a, int left, int right)
{
int mid = get_mid(a, left, right);
swap(&a[mid], &a[left]);
int keyi = left;
while (right > left)
{
while (right > left && a[right] >= a[keyi])
{
right--;
}
while (right > left && a[left] <= a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[keyi], &a[left]);
return left;
}
// 快速排序挖坑法
int part_sort2(int* a, int left, int right)
{
int mid = get_mid(a, left, right);
swap(&a[mid], &a[left]);
int key = a[left];
int hole = left;
while (left < right)
{
while (left < right && a[right] > key)
{
right--;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] < key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
swap(&a[hole], &key);
return hole;
}
// 快速排序前后指针法
int part_sort3(int* a, int left, int right)
{
int mid = get_mid(a, left, right);
swap(&a[mid], &a[left]);
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
prev++;
if (cur != prev)
swap(&a[cur], &a[prev]);
}
cur++;
}
swap(&a[prev], &a[keyi]);
return prev;
}
void quick_sort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = part_sort1(a, left, right);
//int keyi = part_sort2(a, left, right);
//int keyi = part_sort3(a, left, right);
quick_sort(a, left, keyi - 1);
quick_sort(a, keyi + 1, right);
}
代码(非递归)
注:栈的结点定义和各接口函数
void quick_sort_non_r(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = part_sort1(a, left, right);
//int keyi = part_sort2(a, left, right);
//int keyi = part_sort3(a, left, right);
if (keyi + 1 < right)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, left);
}
}
STDestroy(&st);
}
🍕归并排序
概念
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
动图演示
代码
void _merge_sort(int* a, int begin, int end, int* tmp)
{
if (begin == end)
return;
int mid = (begin + end) / 2;
_merge_sort(a, begin, mid, tmp);
_merge_sort(a, mid + 1, end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void merge_sort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
return;
}
_merge_sort(a, 0, n - 1, tmp);
free(tmp);
}
代码(非递归)
void _merge_sort_non_r(int* a, int n, int* tmp)
{
int gap = 1;
int begin = 0;
while (gap < n)
{
int j = 0;
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;
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
}
void merge_sort_non_r(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
return;
}
_merge_sort_non_r(a, n, tmp);
free(tmp);
}
🍭计数排序
概念
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
算法步骤
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 填充目标数组:将每个元素i放在新数组中,每放一个元素就将C(i)减去1,减到0再去放下一个i。
动图演示
代码
void count_sort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
if(count == NULL)
return;
memset(count, 0, sizeof(int) * range);
// 统计次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
// 排序
int k = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[k++] = j + min;
}
}
free(count);
}
🍧排序算法复杂度及稳定性
排序算法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
简单选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
直接插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | O ( n log n ) ~ O ( n 2 ) O(n\log n) ~O(n^2) O(nlogn)~O(n2) | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
堆排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( 1 ) O(1) O(1) | 不稳定 |
快速排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( log n ) ~ O ( n ) O(\log n) ~O(n) O(logn)~O(n) | 不稳定 |
归并排序 | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n log n ) O(n\log n) O(nlogn) | O ( n ) O(n) O(n) | 稳定 |
计数排序 | O ( n + r a n g e ) O(n+range) O(n+range) | O ( n + r a n g e ) O(n+range) O(n+range) | O ( n + r a n g e ) O(n+range) O(n+range) | O ( r a n g e ) O(range) O(range) | 稳定 |