算法小白学习日记-6:七种排序算法

本文详细介绍了选择排序、插入排序、希尔排序、冒泡排序、快速排序、归并排序和基数排序七种常见的排序算法,包括它们的原理、C++代码实现以及时间复杂度、空间复杂度和稳定性分析。
摘要由CSDN通过智能技术生成

本文整理了常用的七种排序算法的原理、代码及复杂度,欢迎批评指正~

概览

本文将介绍选择排序、插入排序、希尔排序、冒泡排序、快速排序、归并排序、基数排序这七种常用排序算法,几种算法的比较如下图所示:

在正式开始前,为了便捷地对每种算法进行测试,我们首先准备一个.h文件,并命名为“0.sort_test.h”,内容如下:

#ifndef _SORT_TEST_H
#define _SORT_TEST_H

#include <string.h>
#include <stdlib.h>
#include <time.h>

#define SMALL_DATA_N 5000
#define BIG_DATA_N   1000000

__attribute__((constructor))//这段语句让该函数可以在主函数main之前执行,而不需要调用,用来初始化时间种子
void __init_Rand__() {
    printf("init rand\n");
    srand(time(0));
}

//该函数用来检查数组是否被正确排序
bool check(int *arr, int l, int r) {
    for (int i = l + 1; i < r; i++) {
        if (arr[i] < arr[i - 1]) return false;
    }
    return true;
}

//该函数用来生成随机数组
int *getRandData(int n) {
    int *arr = (int *)malloc(sizeof(int) * n);
    for (int i = 0; i < n; i++) arr[i] = rand() % 10000;
    return arr;
}

//该宏用来交换两个数字
#define swap(a, b) { \
    __typeof(a) __c = a; \
    a = b, b = __c; \
}

//该宏用来测试排序函数
#define TEST(func, arr, n) { \
    printf("Test %s : ", #func); \
    int *temp = (int *)malloc(sizeof(int) * n); \
    memcpy(temp, arr, sizeof(int) * n); \
    long long b = clock(); \
    func(temp, 0, n); \
    long long e = clock(); \
    if (check(temp, 0, n)) { \
        printf("\tOK "); \
    } else { \
        printf("Failed "); \
    } \
    printf(" %ditems %lldms\n", n, (e - b) * 1000 / CLOCKS_PER_SEC); \
    free(temp);\
}

#endif

1. 选择排序

排序原理

遍历数组,每次找到待排序区最大的元素,放到已排序区。

代码实现

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "0.sort_test.h"

void selection_sort(int *arr, int l, int r) {
    for (int i = l, I = r - 1; i < I; i++) {
        int ind = i;
        for (int j = i + 1; j < r; j++) {
            if (arr[j] < arr[ind]) ind = j;
        }
        swap(arr[i], arr[ind]);
    }
    return ;
}

int main() {
    int *arr = getRandData(SMALL_DATA_N);
    TEST(selection_sort, arr, SMALL_DATA_N);
    free(arr);
    return 0;
}

算法分析

  • 时间复杂度:数组有n个元素,扫描n次,共需要比较n*(n-1)/2次,因此时间复杂度为O(n^2);
  • 空间复杂度:排序中没有开辟额外的存储空间,因此空间复杂度为O(1);
  • 稳定性:排序中每次遍历将最大值调整到待排序区,因此会打乱相同元素的顺序,算法不稳定。

2. 插入排序

排序原理

依次调整每个元素,如果前面的元素大于当前元素,则交换顺序。

代码实现

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "0.sort_test.h"

void insert_sort(int *arr, int l, int r) {
    for (int i = l + 1; i < r; i++) {
        int j = i;
        while (j > l && arr[j] < arr[j - 1]) {
            swap(arr[j], arr[j - 1]);
            j -= 1;
        }
    }
    return ;
}

void unguarded_insert_sort(int *arr, int l, int r) {
    int ind = l;
    for (int i = l + 1; i < r; i++) {
        if (arr[i] < arr[ind]) ind = i;
    }
    while (ind > l) {
        swap(arr[ind], arr[ind - 1]);//将最小的元素移动到数组起始处,避免了j>l条件的判断
        ind -= 1;
    }
    for (int i = l + 1; i < r; i++) {
        int j = i;
        while (arr[j] < arr[j - 1]) {
            swap(arr[j], arr[j - 1]);
            j -= 1;
        }
    }
    return ;
}

int main() {
    int *arr = getRandData(SMALL_DATA_N);
    TEST(insert_sort, arr, SMALL_DATA_N);
    TEST(unguarded_insert_sort, arr, SMALL_DATA_N);
    free(arr);
    return 0;
}

此处展示了两种插入排序算法,第二种为无监督的插入排序。在第一种排序中,需要考虑边界条件"j > l", 而无监督的插入排序则首先将最小的元素移动到数组前端,这样就避免了每次对于"j > l"条件的判断,提高了算法的效率。

算法分析

  • 时间复杂度:最坏的情况是原数组是逆序,则需要调整n * (n - 1) / 2次,复杂度为O(n^2)。最好的情况是原先数组就是顺序,只需要遍历一遍数组,此时复杂度为O(n)。因此,平均的时间复杂度是O(n^2);
  • 空间复杂度:排序中没有开辟额外的存储空间,因此空间复杂度为O(1);
  • 稳定性:排序中不会交换相同元素的顺序,因此算法是稳定的。

3. 希尔排序

排序原理

希尔排序在插入排序的基础上发展而来,首先选择一定的步长,通过步长对数组进行分组,对每组进行插入排序。之后减小步长,重复上述过程。

对于希尔排序的个人理解:对于普通的插入排序,如果原数组越有序,则插入排序的效率越高。希尔排序就是借鉴了这一思想,通过分组排序,使数组整体更加有序,因此提高了效率。

代码实现

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "0.sort_test.h"

void unguarded_insert_sort(int *arr, int l, int r, int step) {
    int ind = l;
    for (int i = l + step; i < r; i += step) {
        if (arr[i] < arr[ind]) ind = i;
    }
    while (ind > l) {//借鉴了无监督的插入排序的思想
        swap(arr[ind], arr[ind - step]);
        ind -= step;
    }
    for (int i = l + 2 * step; i < r; i += step) {
        int j = i;
        while (arr[j] < arr[j - step]) {
            swap(arr[j], arr[j - step]);
            j -= step;
        }
    }
    return ;
}

void shell_sort(int *arr, int l, int r) {
    int k = 2, n = (r - l), step;
    do {
        step = n / k == 0 ? 1 : n / k;
        for (int i = l, I = l + step; i < I; i++) {
            unguarded_insert_sort(arr, i, r, step);
        }
        k *= 2;
    } while (step != 1);
    return ;
}

int main() {
    int *arr = getRandData(BIG_DATA_N);
    TEST(shell_sort,         arr, BIG_DATA_N);
    TEST(shell_sort_hibbard, arr, BIG_DATA_N);
    free(arr);
    return 0;
}

算法分析

  • 时间复杂度:最坏情况的复杂度为O(n^2),最好情况的复杂度为O(nlogn);
  • 空间复杂度:排序中没有开辟额外的存储空间,因此空间复杂度为O(1);
  • 稳定性:因为涉及分组排序,所以有可能交换相同元素的顺序,算法是不稳定的。

4. 冒泡排序

排序原理

遍历数组中的每个元素,如果后面的元素小于前面的元素,则交换两个元素。每次遍历都将最大的元素调整到已排序区。

代码实现

#include <stdio.h>
#include <stdlib.h>
#include "0.sort_test.h"

void bubble_sort(int *arr, int l, int r) {
    for (int i = r - 1, I = l + 1, cnt; i >= I; i--) {
        cnt = 0;
        for (int j = l; j < i; j++) {
            if (arr[j] <= arr[j + 1]) continue;
            swap(arr[j], arr[j + 1]);
            cnt += 1;
        }
        if (cnt == 0) break;//扫描一遍后如果是顺序,则直接跳出
    }
    return ;
}

int main() {
    int *arr = getRandData(SMALL_DATA_N);
    TEST(bubble_sort, arr, SMALL_DATA_N);
    free(arr);
    return 0;
}

算法分析

  • 时间复杂度:最坏的情况是原数组是逆序,则需要调整n * (n - 1) / 2次,复杂度为O(n^2)。最好的情况是原先数组就是顺序,只需要遍历一遍数组,此时复杂度为O(n)。因此,平均的时间复杂度是O(n^2);
  • 空间复杂度:排序中没有开辟额外的存储空间,因此空间复杂度为O(1);
  • 稳定性:排序中不会交换相同元素的顺序,因此算法是稳定的。

5. 快速排序

排序原理

快速排序首先选择一个基准值,后将剩余元素分成两个区域,一个区域的元素都小于基准值,一个区域的元素都大于基准值,将基准值插入到两个区域的中间,后分别对两个区域进行快速排序。

代码实现

#include <stdio.h>
#include <stdlib.h>
#include "0.sort_test.h"

void quick_sort(int *arr, int l, int r) {
    if (r - l <= 2) {//当元素较少时,直接排序
        if (r - l <= 1) return ;
        if (arr[l] > arr[l + 1]) swap(arr[l], arr[l + 1]);
        return ;
    }
    int x = l, y = r - 1, z = arr[l];
    while (x < y) {
        while (x < y && z <= arr[y]) --y;
        if (x < y) arr[x++] = arr[y];
        while (x < y && arr[x] <= z) ++x;
        if (x < y) arr[y--] = arr[x];
    }
    arr[x] = z;
    quick_sort(arr, l, x);
    quick_sort(arr, x + 1, r);
    return ;
}

int main() {
    int *arr_s = getRandData(SMALL_DATA_N);
    int *arr_b = getRandData(BIG_DATA_N);
    TEST(quick_sort, arr_s, SMALL_DATA_N);
    TEST(quick_sort, arr_b, BIG_DATA_N);
    free(arr_s);
    free(arr_b);
    return 0;
}

算法分析

  • 时间复杂度:最好的情况是每次进行分区操作时可以均分数组,此时的时间复杂度为O(nlogn);最坏的情况分区极度不平衡,此时的时间复杂度为O(n^2);
  • 空间复杂度:跟实现的方法有关;
  • 稳定性:排序过程中会将基准值插入到两个区间中间,因此有可能破坏相同元素的顺序,算法是不稳定的。

6. 归并排序

排序原理

将数组分成个子数字,并继续将子数组分成两份,直到没法再分。之后对数组进行合并,合并每两个数组时,保证合并的顺序是从小到大。

代码实现

#include <stdio.h>
#include <stdlib.h>
#include "0.sort_test.h"

int *buff;
void merge_sort(int *arr, int l, int r) {
    if (r - l <= 1) return ;//递归终止条件
    int mid = (l + r) / 2;
    merge_sort(arr, l, mid);//左段递归
    merge_sort(arr, mid, r);//右段递归
    // merge
    int p1 = l, p2 = mid, k = 0;
    while (p1 < mid || p2 < r) {//利用左右两个指针进行合并
        if (p2 == r || (p1 < mid && arr[p1] <= arr[p2])) {
            buff[k++] = arr[p1++];
        } else {
            buff[k++] = arr[p2++];
        }
    }
    for (int i = l; i < r; i++) arr[i] = buff[i - l];
    return ;
}

int main() {
    int *arr_s = getRandData(SMALL_DATA_N);
    int *arr_b = getRandData(BIG_DATA_N);
    buff = (int *)malloc(sizeof(int) * BIG_DATA_N);
    TEST(merge_sort, arr_s, SMALL_DATA_N);
    TEST(merge_sort, arr_b, BIG_DATA_N);
    free(arr_s);
    free(arr_b);
    free(buff);
    return 0;
}

算法分析

  • 时间复杂度:每次将数组从中间分成两份,因此时间复杂度为O(nlogn);
  • 空间复杂度:O(n);
  • 稳定性:在排序中可以保证大小相同元素的前后顺序不变,因此算法稳定。

7. 基数排序

排序原理

基数排序给人一种用空间换时间的感觉,如下图所示,对于10以上的数组,我们可以首先只关注其个位,通过基数排序保证其个位数字从大到小排列。之后再对十位的数字进行基数排序,另外因为基数排序是稳定的,因此在对十位进行排序时,不会打断个位的排序结果。以此类推,直到把所有位数都排序完。

再进一步,在基数排序时也不一定非要按照数字的位数进行排序,可以选择一个数字,比如下面代码中选择的是65536(因为int类型最大是2的32次方,而65536是2的16次方,一个int类型的数字除以65536的结果一定小于65536)。之后开辟一个65536大小的存储空间,首先对每个元素除以65536的余数排序,后对每个元素除以65536的结果排序,只需要两次遍历,就可以将数组排序。

代码实现

#include <stdio.h>
#include <stdlib.h>
#include "0.sort_test.h"

void radix_sort(int *arr, int l, int r) {
    #define K 65536
    int *cnt  = (int *)malloc(sizeof(int) * K);
    int *temp = (int *)malloc(sizeof(int) * (r - l));
    // round 1
    memset(cnt, 0, sizeof(int) * K);
    for (int i = l; i < r; i++) cnt[arr[i] % K] += 1;
    for (int i = 1; i < K; i++) cnt[i] += cnt[i - 1];
    for (int i = r - 1; i >= l; i--) temp[--cnt[arr[i] % K]] = arr[i];
    memcpy(arr + l, temp, sizeof(int) * (r - l));
    // round 2
    memset(cnt, 0, sizeof(int) * K);
    for (int i = l; i < r; i++) cnt[arr[i] / K] += 1;
    for (int i = 1; i < K; i++) cnt[i] += cnt[i - 1];
    for (int i = r - 1; i >= l; i--) temp[--cnt[arr[i] / K]] = arr[i];
    memcpy(arr + l, temp, sizeof(int) * (r - l));
    return ;
}

int main() {
    int *arr_s = getRandData(SMALL_DATA_N);
    int *arr_b = getRandData(BIG_DATA_N);
    TEST(radix_sort, arr_s, SMALL_DATA_N);
    TEST(radix_sort, arr_b, BIG_DATA_N);
    free(arr_s);
    free(arr_b);
    return 0;
}

算法分析

  • 时间复杂度:根据基数排序原理可知复杂度为O(n*K);
  • 空间复杂度:O(n + K);
  • 稳定性:在排序中可以保证大小相同元素的前后顺序不变,因此算法稳定。

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值