0
点赞
收藏
分享

微信扫一扫

C 数据结构之十大排序

拾杨梅记 2022-07-18 阅读 98


C 数据结构之十大排序

排序算法是《数据结构与算法》中最基本的算法之一。

排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:

C 数据结构之十大排序_数组

关于时间复杂度

  • 平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
  • 线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
  • O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
  • 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。

关于稳定性

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。

不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

名词解释

n:数据规模

k:“桶”的个数

In-place:占用常数内存,不占用额外内存

Out-place:占用额外内存

稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同

冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

算法步骤

比较相邻的元素。如果第一个比第二个大,就交换他们两个。

对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

针对所有的元素重复以上的步骤,除了最后一个。

持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

什么时候最快

当输入的数据已经是正序时(都已经是正序了,我还要你冒泡排序有何用啊)。

什么时候最慢

当输入的数据是反序时(写一个 for 循环反序输出数据不就行了,干嘛要用你冒泡排序呢,我是闲的吗)。

复杂程度

时间复杂度O(n^2) 空间复杂度O(1)

空间复杂度O(n^2)

动图演示

C 数据结构之十大排序_i++_02

代码

#include <stdio.h>

void bubble_sort(int a[], int size);
void show(int *arr, int len);

int main()
{
int a[7] = {1, 2, 4, 5, 3, 9, 8};
//数组长度
int num = sizeof(a) / sizeof(a[0]);

bubble_sort(a, num);

show(a, num);

return 0;
}

void bubble_sort(int a[], int size)
{
for (int i = 0; i < size - 1; ++i)
{
for (int j = 0; j < size - 1 - i; ++j)
{
if (a[j] > a[j + 1])
{
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
}
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

算法步骤

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

复杂程度

时间复杂度O(n^2) 空间复杂度O(1)

空间复杂度O(1)

动图演示

C 数据结构之十大排序_i++_03

代码

#include <stdio.h>

void select_sort(int *arr, int len);
void show(int *arr, int len);

int main()
{
int drr[] = {1, 8, 7, 5, 0};
//sizeof()它的作用是返回一个对象或类型所占的内存的字节数
int num = sizeof(drr) / sizeof(drr[0]);
select_sort(drr, num);
show(drr, num);
return 0;
}

void select_sort(int *arr, int len)
{
int i, j;
int tmp;
int min = 0;
for (i = 0; i < len; i++)
{
int min = i;
for (j = i + 1; j < len; j++)
{
if (arr[j] < arr[min])
{
tmp = arr[min];
arr[min] = arr[j];
arr[j] = tmp;
}
}
}
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

插入排序

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

从第一个元素开始,该元素可以认为已经被排序;
取出下一个元素,在已经排序的元素序列中从后向前扫描;
如果该元素(已排序)大于新元素,将该元素移到下一位置;
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
将新元素插入到该位置后;
重复步骤2~5。

复杂程度

时间复杂度O(n^2) 空间复杂度O(1)

动图演示

C 数据结构之十大排序_数组_04

代码实现

#include <stdio.h>

void insertion_sort(int *arr, int size);
void show(int *arr, int len);

int main()
{
int a[7] = {1, 2, 4, 5, 3, 9, 8};
//数组长度
int num = sizeof(a) / sizeof(a[0]);

insertion_sort(a, num);

show(a, num);

return 0;
}

void insertion_sort(int *arr, int size)
{
int i, j, tmp;
for (i = 1; i < size; i++)
{
if (arr[i] < arr[i - 1])
{
tmp = arr[i];
for (j = i - 1; j >= 0 && arr[j] > tmp; j--)
{
arr[j + 1] = arr[j];
}
arr[j + 1] = tmp;
}
}
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

希尔排序

希尔排序(Shell Sort)
1959年Shell发明,第一个突破O(n^2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。

也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

算法描述

  • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

复杂程度

时间复杂度O(n^1.3) 空间复杂度O(1)

代码

#include <stdio.h>

void shell_sort(int *arr, int len);
void show(int *arr, int len);

int main()
{
int drr[] = {1, 8, 7, 5, 0};
//sizeof()它的作用是返回一个对象或类型所占的内存的字节数
int num = sizeof(drr) / sizeof(drr[0]);
shell_sort(drr, num);
show(drr, num);
return 0;
}

void shell_sort(int *arr, int size)
{
int i, j, tmp, increment;
for (increment = size / 2; increment > 0; increment /= 2)
{
for (i = increment; i < size; i++)
{
tmp = arr[i];
for (j = i - increment; j >= 0 && tmp < arr[j]; j -= increment)
{
arr[j + increment] = arr[j];
}
arr[j + increment] = tmp;
}
}
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
自下而上的迭代;
在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:

However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.

然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。

说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

算法步骤

申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

设定两个指针,最初位置分别为两个已经排序序列的起始位置;

比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;

重复步骤 3 直到某一指针达到序列尾;

将另一序列剩下的所有元素直接复制到合并序列尾。

复杂程度

时间复杂度O(nlog2n) 空间复杂度O(n)

动图演示

C 数据结构之十大排序_数据_05

代码

#include <stdio.h>

#define MAXSIZE 100

void merge(int *list1, int list1_size, int *list2, int list2_size);
void merge_sort(int k[], int n);
void show(int *arr, int len);

int main()
{
int drr[] = {1, 8, 7, 5, 0};
//sizeof()它的作用是返回一个对象或类型所占的内存的字节数
int num = sizeof(drr) / sizeof(drr[0]);
merge_sort(drr, num);
show(drr, num);
return 0;
}

// 递归的方式实现归并排序

// 实现归并,并把结果存放到list1
void merge(int *list1, int list1_size, int *list2, int list2_size)
{
int i, j, k, m;
int temp[MAXSIZE];

i = j = k = 0;

while (i < list1_size && j < list2_size)
{
if (list1[i] < list2[j])
{
temp[k] = list1[i];
k++;
i++;
}
else
{
temp[k++] = list2[j++];
}
}

while (i < list1_size)
{
temp[k++] = list1[i++];
}

while (j < list2_size)
{
temp[k++] = list2[j++];
}

for (m = 0; m < (list1_size + list2_size); m++)
{
list1[m] = temp[m];
}
}

void merge_sort(int k[], int n)
{
if (n > 1)
{
/*
*list1是左半部分,list2是右半部分
*/
int *list1 = k;
int list1_size = n / 2;
int *list2 = k + list1_size;
int list2_size = n - list1_size;

merge_sort(list1, list1_size);
merge_sort(list2, list2_size);

// 把两个合在一起
merge(list1, list1_size, list2, list2_size);
}
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

快速排序

快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

复杂程度

时间复杂度O(nlog2n) 空间复杂度O(nlog2n)

动图演示

C 数据结构之十大排序_数组_06

代码实现

#include <stdio.h>

void quick_sort(int *arr, int maxlen, int begin, int end);
void swap(int *a, int *b);
void show(int *arr, int len);

int main()
{
int drr[] = {1, 8, 7, 5, 0};
int num = sizeof(drr) / sizeof(drr[0]);
quick_sort(drr, num, 0, num - 1);
show(drr, num);
return 0;
}

void quick_sort(int *arr, int maxlen, int begin, int end)
{
int i, j;
if (begin < end)
{
i = begin + 1;
j = end;
while (i < j)
{
if (arr[i] > arr[begin])
{
swap(&arr[i], &arr[j]);
j--;
}
else
{
i++;
}
}
if (arr[i] >= arr[begin])
{
i--;
}
swap(&arr[begin], &arr[i]);
quick_sort(arr, maxlen, begin, i);
quick_sort(arr, maxlen, j, end);
}
}

void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

堆排序

堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

复杂程度

时间复杂度O(nlog2n) 空间复杂度O(1)

动图演示

C 数据结构之十大排序_数组_07

代码

#include <stdio.h>
#include <stdlib.h>

void show(int *arr, int len);
void swap(int *a, int *b);
void max_heapify(int arr[], int start, int end);
void heap_sort(int arr[], int len);

int main()
{
int arr[] = {1, 2, 4, 5, 3, 9, 8};
int num = (int)sizeof(arr) / sizeof(*arr);
heap_sort(arr, num);
show(arr, num);
return 0;
}

void swap(int *a, int *b)
{
int temp = *b;
*b = *a;
*a = temp;
}

void max_heapify(int arr[], int start, int end)
{
// 建立父节点指标和子节点指标
int dad = start;
int son = dad * 2 + 1;
while (son <= end)
{ // 若子节点指标在范围内才做比较
if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比较两个子节点大小,选择最大的
son++;
if (arr[dad] > arr[son]) //如果父节点大于子节点代表调整完毕,直接跳出函数
return;
else
{ // 否则交换父子内容再继续子节点和孙节点比较
swap(&arr[dad], &arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}

void heap_sort(int arr[], int len)
{
int i;
// 初始化,i从最后一个父节点开始调整
for (i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
// 先将第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
for (i = len - 1; i > 0; i--)
{
swap(&arr[0], &arr[i]);
max_heapify(arr, 0, i - 1);
}
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

计数排序

计数排序(Counting Sort)
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

算法描述

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

复杂程度

时间复杂度O(n+k) 空间复杂度O(n+k)

动图演示

C 数据结构之十大排序_i++_08

代码

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

void count_sort(int *a, int len);
void show(int *arr, int len);

int main()
{
int a[] = {3, 4, 3, 2, 1, 2, 6, 5, 4, 7};
printf("排序前:");
show(a, sizeof(a) / sizeof(int));
count_sort(a, sizeof(a) / sizeof(int));
printf("排序后:");
show(a, sizeof(a) / sizeof(int));
return 0;
}

//计数排序
void count_sort(int *a, int len)
{
assert(a);
//通过max和min计算出临时数组所需要开辟的空间大小
int max = a[0], min = a[0];
for (int i = 0; i < len; i++)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
//使用calloc将数组都初始化为0
int range = max - min + 1;
int *b = (int *)calloc(range, sizeof(int));
//使用临时数组记录原始数组中每个数的个数
for (int i = 0; i < len; i++)
{
//注意:这里在存储上要在原始数组数值上减去min才不会出现越界问题
b[a[i] - min] += 1;
}
int j = 0;
//根据统计结果,重新对元素进行回收
for (int i = 0; i < range; i++)
{
while (b[i]--)
{
//注意:要将i的值加上min才能还原到原始数据
a[j++] = i + min;
}
}
//释放临时数组
free(b);
b = NULL;
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

桶排序

桶排序(Bucket Sort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

算法描述

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

复杂程度

时间复杂度O(n+k) 空间复杂度O(n+k)

代码

#include <iostream>
#include <stdio.h>

void bucket_sort(int *arr, int size, int max);
void show(int *arr, int len);

int main()
{
int a[7] = {1, 2, 4, 5, 3, 9, 8};
//数组长度
int num = sizeof(a) / sizeof(a[0]);

bucket_sort(a, num, num);

show(a, num);

return 0;
}

void bucket_sort(int *arr, int size, int max)
{
int i, j;
int buckets[max];
memset(buckets, 0, max * sizeof(int));
for (i = 0; i < size; i++)
{
buckets[arr[i]]++;
}
for (i = 0, j = 0; i < max; i++)
{
while ((buckets[i]--) > 0)
arr[j++] = i;
}
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}

基数排序

基数排序(Radix Sort)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

算法描述

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

复杂程度

时间复杂度O(n*k) 空间复杂度O(n+k)

代码

#include <stdio.h>
#include <string.h>
#define N 10 //数组长度
#define D 10 //最大位数

void show(int *arr, int len);
int get_digit(int M, int i);
void radix_sort(int num[], int len);

int main()
{
int a[7] = {1, 2, 4, 5, 3, 9, 8};
//数组长度
int num = sizeof(a) / sizeof(*a);

radix_sort(a, num);

show(a, num);
return 0;
}

int get_digit(int M, int i) //取整数M的第i位数
{
while (i > 1)
{
M /= 10;
i--;
}
return M % 10;
}

void radix_sort(int num[], int len)
{
int i, j, k, l, digit;
int allot[10][N]; //《分配数组》

memset(allot, 0, sizeof(allot)); //初始化《分配数组》

for (i = 1; i <= D; i++)
{
int flag = 0;
//分配相应位数的数据,并存入《分配数组》
for (j = 0; j < len; j++)
{
digit = get_digit(num[j], i);
k = 0;
while (allot[digit][k])
k++;
allot[digit][k] = num[j];
if (digit) //判断是否达到了最高位数
flag = 1;
}
if (!flag) //如果数组每个数的第i位都为零
break; //即可直接退出循环

//将《分配数组》的数据依次收集到原数组中
l = 0;
for (j = 0; j < 10; j++)
{
k = 0;
while (allot[j][k] > 0)
{
num[l++] = allot[j][k];
k++;
}
}
//每次分配,收集后初始化《分配数组》,用于下一位数的分配和收集
memset(allot, 0, sizeof(allot));
}
}

void show(int *arr, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}


举报

相关推荐

0 条评论