常见排序方法 <十种排序方法>

交流群详见下方群名片,欢迎加入讨论,群内多企业大佬、高校学生。

目录

一、插入排序

1. 简单插入排序 

🍉 原理介绍:🍉 特点:🍉 代码示例:普通插入排序🍉 算法复杂度:🍉 改进方案:

2. 希尔排序

🍅 原理介绍🍅 特点🍅 代码示例🍅 算法复杂度(参考《数据结构》严蔚敏)🍅 改进方案

二、交换排序

1. 冒泡排序

🍊 原理介绍:🍊 特点:🍊 代码示例:🍊 算法复杂度:🍊 改进方案:

2. 快速排序

                  🍎 原理简介:🍎 特点:🍎 代码示例:数组排序🍎 代码示例:单链表排序🍎 算法复杂度:🍎 改进方案:

三、选择排序

1. 简单选择排序

🍋 原理介绍:🍋 特点:🍋 代码示例:🍋 算法复杂度:🍋 改进方案:

2. 堆排序 

🍈 原理介绍:🍈 特点:🍈 代码示例:🍈 算法复杂度:🍈 改进方案:

四、归并排序

🥝 原理介绍:🥝 特点:🥝 代码示例:🥝 算法复杂度:🥝 改进方案:

五、非比较排序

1. 计数排序

🌽 原理介绍:   🌽 特点:  🌽 代码示例:  🌽 算法复杂度:

2. 桶排序

🥦 原理介绍:   🥦 特点:   🥦 代码示例:  🥦 算法复杂度:

3. 基数排序

🥔 原理介绍:🥔 特点:🥔 代码示例:🥔 算法复杂度:


一、插入排序

1. 简单插入排序 

🍉 原理介绍:

一分钟掌握“插入排序”_哔哩哔哩_bilibili

它的基本思想是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序。

🍉 特点:

  1. 稳定:插入排序是一种稳定的排序算法,它能够保证在排序过程中相等元素的相对位置不变。

  2. 最坏情况下比较n*(n-1)/2次,最好情况下比较n-1次;

  3. 第k次排序后,前k个元素已经是排好序的。

🍉 代码示例:普通插入排序

/*****************************************************
 * @description: ordinary insert
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/25
******************************************************/
#include <iostream>
using namespace std;

void insert_sort(int array[], int num)
{
    int key, j;
    // 从第二个元素开始,默认第一个元素有序
    for(int i = 1; i < num; i++)
    {
        key = array[i];
        j = i - 1;
        while(j >= 0 && key < array[j])
        {
            array[j + 1] = array[j];
            j--;
        }
        array[j+ 1] = key;
    }
}

void display(int array[], int num)
{
    for(int i = 0; i < num; i++)
    {
        cout<< array[i]<< " ";
    }
    cout<<endl;
}

int main()
{
    int array[] = {5, 2, 4, 6, 1, 3};
    int num = sizeof(array) / sizeof(array[0]);

    display(array, num);

    insert_sort(array, num);

    display(array, num);
    return 0;
}

🍉 算法复杂度:

最好时间复杂度:O(N) 有序

最坏时间复杂度:O(N2)

平均时间复杂度:O(N2)

🍉 改进方案:

二分(折半)插入排序:二分插入排序是插入排序的另一种改进版本,它通过使用二分查找来确定待插入元素的位置,从而减少比较的次数。

二分插入排序:

/*****************************************************
 * @description: binary insert sort
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/25
******************************************************/
#include <iostream>
using namespace std;

// 折半插入排序
void binary_insert_sort(int array[], int num)
{
    int key;
    int left, right, mid = 0;
    // 从第二个元素开始,默认第一个元素有序
    for(int i = 1; i < num; i++)
    {
        key = array[i];

        left = 0;
        right = i - 1;
        while(left <= right)// 第一次需要等号
        {
            mid = (left + right) / 2;
            if(array[mid] > key)right = mid - 1;// key一定要放在mid的左边
            else left = mid + 1;// key一定要放在mid的右边
            cout<< left<< " "<< right<<endl;
        }
        cout<<endl;

        // left和right最终一定会指向同一个元素,然后再执行一次while后跳出
        // 如果这个元素大于key,right = mid - 1,left指向最后一个大于key的元素
        // 如果这个元素小于key,left = mid + 1,left指向最后一个大于key的元素
        for(int j = i - 1; j >= left; j--)
        {
            array[j + 1] = array[j];// 当前元素左侧比key大的元素往左移,覆盖掉key所在位置   
        }
        array[left] = key;// 将当前待排序元素插入到指定位置
    }
}

void display(int array[], int num)
{
    for(int i = 0; i < num; i++)
    {
        cout<< array[i]<< " ";
    }
    cout<<endl;
}

int main()
{
    int array[] = {5, 2, 4, 6, 1, 3, 20, 35, 16, 164, 28, 46, 69};
    int num = sizeof(array) / sizeof(array[0]);

    display(array, num);

    binary_insert_sort(array, num);

    display(array, num);
    return 0;
}

2. 希尔排序

🍅 原理介绍

[算法]六分钟彻底弄懂希尔排序,简单易懂_哔哩哔哩_bilibili

秒懂算法3-希尔排序_哔哩哔哩_bilibili

希尔排序(Shell Sort)是插入排序的一种改进算法。它与插入排序的不同之处在于,它会优先比较距离较远的元素,而不是相邻的元素。这样可以帮助减少数据移动的次数,提高排序的效率。它的基本思想是:

  1. 通过设置一个增量序列,将原序列分割成若干子序列。
  2. 对于每个子序列,使用直接插入排序算法进行排序。
  3. 随着增量逐渐减小,整个序列也越来越趋于有序。

增量序列的选择对希尔排序的性能有很大影响。

🍅 特点

  1. 希尔排序是不稳定的排序算法,它并不保证相同元素之间的相对位置不变。
  2. 希尔排序是一种较高效的排序算法,在大多数情况下都能达到较优的性能。
  3. 希尔排序可以在线性时间内完成对小数据集的排序,对于大数据集使用希尔排序也有较好的性能。

🍅 代码示例

/*********************************************************
 * @description: shell sort
 * @author: kashine
 * @version: 1.0 
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/26
 * ******************************************************/

#include <iostream>
using namespace std;

// 希尔排序
// 每一个gap下分为好多组
void shellSort(int* arr, int n)
{
    // gap 为步长,每次减为原来的一半
    // 这里采用朴素希尔增量,就是每次增量都是原来的一半,直到增量为1为止
    // 每一次循环都通过不断缩短增量达到排序的效果
    for (int gap = n / 2; gap > 0; gap /= 2)
    {
        //下面的内容和插入排序的原理是一样的,只不过每个待排序元素的间隔是gap
        // 共 gap 个组,对每组进行插入排序
        // 疑问:这里为何不从0开始?
        // 解答:简单插入排序的时候,认为第一个元素(下标0)有序,
        // 从第2个元素开始(下标1),遍历右侧元素,插入到已排序元素的合适位置
        // 注意:这里0算是有序,gap,2gap,3gap...算是无序(第一组)
        //      这里1算是有序,gap+1,2gap+1,3gap+1...算是无序(第二组)
        for (int i = gap; i < n; i++)
        {
            int j = i - gap;// 指向从右侧往左第一个有序元素
            int temp = arr[i];// 左侧第一个无序元素
            while (j >= 0 && temp < arr[j])// 如果有序序列的内容大于temp,把有序元素右移一
            {
                arr[j + gap] = arr[j];// 往右侧赋值
                j -= gap;// 步长是gap
            }
            arr[j + gap] = temp;// 跳出while循环的时候j所在元素是第一个小于temp的元素,temp应放在它右面
        }
    }
}

int main()
{
    int arr[] = {3, 1, 5, 7, 2, 4, 9, 6};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 调用希尔排序
    shellSort(arr, n);

     // 输出排序后的结果
    for (int i = 0; i < n; i++)
        cout << arr[i] << " ";
    cout << endl;

    return 0;
}

🍅 算法复杂度(参考《数据结构》严蔚敏)

最好时间复杂度:O(?)

最坏时间复杂度:O(?)

平均时间复杂度:O(N1.3)

空间复杂度:O(1)

《数据结构》严蔚敏中提到:“当增量大于1时,关键字较小的记录就不是一步一步地挪动,而是跳跃式地移动,从而使得在进行最后一趟增量为1的插入排序中,序列已基本有序,只要做记录的少量比较和移动即可完成排序,因此希尔排序的时间复杂度较直接插入排序低。但要具体进行分析,则是一个复杂的问题,因为希尔排序的时间复杂度是所取”增量“序列的函数,这涉及一些数学上尚未解决的难题。因此,到目前为止尚未有人求得一种最好的增量序列。

🍅 改进方案

  • 一种优化方法就是使用更高性能的递增序列
  • 因为是基于插入排序,所以希尔排序的优化可以借鉴简单插入排序的优化方法处理,比如折半插入排序。

二、交换排序

1. 冒泡排序

🍊 原理介绍:

排序算法1-冒泡排序_哔哩哔哩_bilibili

冒泡排序的基本思想是:重复地遍历数组,每次比较相邻的两个数,如果它们的顺序不对,就交换它们的位置。这个算法的名字由来是因为越小(或者越大)的元素会经由交换慢慢“浮”到数列的顶端。

🍊 特点:

  1. 实现简单:它是一个简单的暴力枚举算法,易于理解。
  2. 时间复杂度高:它的时间复杂度是O(n^2),意味着它在处理大型数据集时效率会很低。
  3. 稳定:它是一个稳定的排序算法,意味着它不会改变原本相同的数的相对顺序。

🍊 代码示例:

/********************************************************
 * @description: bubble sort
 * @author: cashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/24
*/
#include <iostream>
#include <vector>
using namespace std;

#define elemtype int

vector<elemtype> unorder = {3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};

int main()
{
    // 冒泡排序
    for(int i = 0; i < unorder.size() - 1; i++)
    {
        for(int j = 0; j < unorder.size() - 1 - i; j++)
        {
            if(unorder[j] > unorder[j + 1])swap(unorder[j], unorder[j + 1]);
        }
    }
    
    for(auto& it : unorder)cout<< it<<" ";
    cout<<endl;
    return 0;
}

🍊 算法复杂度:

  • 最好时间复杂度:O(N) 改进后
  • 最坏时间复杂度:O(N2)
  • 平均时间复杂度:O(N2)

如果输入数据已经有序,那么冒泡排序的时间复杂度就是最优的,为 O(n),即只需要比较 n-1 次就可以完成排序。但如果输入数据是无序的,那么冒泡排序的时间复杂度就是最坏的,为 O(n^2)。冒泡排序会进行 n(n-1)/2 次比较(等差),因此时间复杂度为 O(n^2)。

🍊 改进方案:

  1. 双向冒泡排序:在每次比较中,不仅向右比较相邻的元素,还向左比较相邻的元素。如果按照排序结果从小到大输出,可以按照“较大气泡从左到右移动,较小气泡从右到左移动”来实现双向冒泡排序的效果。详见。

  2. 利用有序:第一轮排序过后的有序区长度是1,第二轮排序过后的有序 区长度是2 。 实际上,数列真正的有序区可能会大于这个长度,因此后面的许多次元素比较是没有意义的。 如何避免这种情况呢?我们可以在每一轮排序的最后,记录下最后一次元素交换的位置,那个位置也就是无序数列的边界,再往后就是有序区了。

  3. 使用标志位:设置一个标志位,如果在一次排序过程中没有发生交换,说明数组已经有序,就可以提前终止排序过程。

第二点代码:

#include <iostream>

void bubble_sort(int arr[], int n) {
    // 记录最后一次交换的位置
    int last_swap_pos = 0;
    // 排序的轮数
    int sort_round = 0;
    // 无序区的边界,初始值为数组的末尾
    int boundary = n - 1;
    while (boundary > 0) {
        // 记录最后一次交换的位置
        last_swap_pos = 0;
        // 对无序区进行排序
        for (int i = 0; i < boundary; i++) {
            if (arr[i] > arr[i + 1]) {
                std::swap(arr[i], arr[i + 1]);
                // 更新最后一次交换的位置
                last_swap_pos = i;
            }
        }
        // 更新无序区的边界
        boundary = last_swap_pos;
        // 统计排序的轮数
        sort_round++;
    }

    std::cout << "排序的轮数: " << sort_round << std::endl;
    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {5, 4, 3, 2, 1};
    int n = sizeof(arr) / sizeof(int);

    std::cout << "排序前: ";
    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    bubble_sort(arr, n);

    return 0;
}

第三点代码:

void bubble_sort(int arr[], int n) {
    bool is_sorted = false;
    for (int i = 0; i < n - 1; i++) {
        if (is_sorted) {
            break;
        }
        is_sorted = true;
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                std::swap(arr[j], arr[j + 1]);
                is_sorted = false;
            }
        }
    }
}

2. 快速排序

🍎 原理简介:

秒懂算法6-快速排序_哔哩哔哩_bilibili

快速排序是一种分治算法,由冒泡法改进而来,用于对一个数组排序。它的基本思想是:

  1. 从数组中选择一个元素,称为“枢轴”(pivot)。
  2. 将数组划分成两个部分,使得:
    • 左部分的所有元素都小于等于枢轴。
    • 右部分的所有元素都大于等于枢轴。
  3. 对左右两部分分别递归地使用快速排序。

🍎 特点:

  • 原地排序:快速排序只需要很少的额外空间,因为它在原地排序。
  • 高效:快速排序的时间复杂度为 O(n*log(n)),通常比其他的排序算法更快。

  • 不稳定:快速排序是一种不稳定的排序算法,这意味着如果数组中有两个元素的值相等,它们的相对顺序可能会在排序后改变。

  • 分治法:快速排序使用分治法来实现排序。它通过递归地将数组分成较小的子数组来实现排序。

  • 递归:快速排序是一种递归算法,它通过不断地调用自身来实现排序。

🍎 代码示例:数组排序

/***************************************
 * @description: ordinary quick sort (array)
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/23
*/

#include <iostream>
using namespace std;

#define elemtype int

elemtype input[15] = {3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};

// 对输入数组input进行快速排序
// low: 需要排序的最低下标
// high: 需要排序的最高下标
void quick_sort(elemtype* input, int low, int high)
{
    if(low > high)return;
    int i = low, j = high, pivot = input[low];
    while(i < j)
    {
        // 从右侧选出比pivot小的元素
        while(i < j && input[j] > pivot)j--;
        // 如果成功找到了比pivot小的元素就放到左侧
        if(i < j)input[i++] = input[j];

        // 从左侧选出比pivot大的元素
        while(i < j && input[i] < pivot)i++;
        // 如果成功找到了比pivot大的元素就放到右侧
        if(i < j)input[j--] = input[i];

        input[i] = pivot;
    }
    quick_sort(input, low, i - 1);
    quick_sort(input, i + 1, high);
}

// 输出数组
void display(elemtype* input, int size)
{
    for(int i = 0; i < size; i++)
    {
        cout<< input[i]<< " ";
    }
    cout<<endl;
}
int main()
{
    display(input, 15);
    quick_sort(input, 0, 14);
    display(input, 15);

    return 0;
}

🍎 代码示例:单链表排序

/*******************************************************************
 * @description: quick sort (list)
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/23
********************************************************************/

#include<iostream>
using namespace std;

#define elemtype int

struct list_node
{
    elemtype val;
    list_node* next;
};

elemtype input[] = {3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};

// 尾插法创建单链表
void create_list(list_node*& list, elemtype input[], int ele_number)
{
    list = new list_node;// 头节点
    list->next = NULL;
    list_node* r = list;// 尾指针
    list_node* q = NULL;
    for(int i = 0; i < ele_number; i++)
    {
        q = new list_node;
        q->val = input[i];
        q->next = NULL;
        r->next = q;
        r = q;
    }
    list = list->next;// 删除头节点
}

// 交换链表的值
void swap_val(elemtype& a, elemtype& b)
{
    elemtype temp = a;
    a = b;
    b = temp;
}

// 输出
void display_list(list_node* list)
{
    list_node* p = list;
    while(p)
    {
        cout<< p->val<< " ";
        p = p->next;
    }
    cout<<endl;
}

// 快速排序
void quick_sort(list_node* start, list_node* end)
{
    if(!start || !end || start == end)return;//头和尾都不能为空,并且不能指向同一位置

    list_node* p1 = start;// 两个指针,分别维护枢轴数左右两侧
    list_node* p2 = start->next;
    int pivot = start->val;// 枢轴数

    while(p2 != end->next)// 不断向后找
    {
        if(p2->val < pivot)// 如果当前p2指向的内容小于pivot
        {
            p1 = p1->next;// p1前进,start先不管
            if(p1 != p2)// 如果两个指针指向同一个地方,就是说已经遍历到p2左侧全部小于pivot了
            {
                swap(p1->val, p2->val);// 将小的放到左侧
            }
        }
        p2 = p2->next;
    }

    // pivot放到中间
    swap(p1->val, start->val);// pivot放到中间,上面没有动他,他一直在start的位置

    // 左半边
    quick_sort(start, p1);

    // 右半边
    quick_sort(p1->next, end);
}

// 快速排序
int main()
{
    list_node* list;
    create_list(list, input, 15);// 创建链表并删除头结点

    // 寻找尾结点
    list_node* r_list = list;
    while(r_list->next)r_list = r_list->next;
    
    display_list(list);
    quick_sort(list, r_list);
    display_list(list);
    return 0;
}

🍎 算法复杂度:

参考排序算法之 快速排序 及其时间复杂度和空间复杂度 快速排序时间复杂度分析 - 知乎

  • 在最好的情况下,数组已经排好序,时间复杂度就是 O(n*log(n))。
  • 在最坏的情况下,数组是完全逆序的,那么快速排序的时间复杂度就是 O(n^2)。这种情况下,每次递归都会分成两个只有一个元素的子数组,这样就需要进行 n 次递归,每次递归都需要进行一次比较,因此总时间复杂度就是 O(n^2)。)
  • 平均情况下,数组是随机排列的,那么快速排序的时间复杂度就是 O(n*log(n))。
  • 快速排序的空间复杂度是 O(log(n)),因为它使用递归实现,需要保存递归时每层的变量。

🍎 改进方案:

来自:快速排序 改进快排的方法_dpkirin的博客-CSDN博客

1、选取随机数作为枢轴。

2、使用左端,右端和中心的中值做为枢轴元。

3、每次选取数据集中的中位数做枢轴。

4、快速排序在处理小规模数据时的表现不好,这个时候可以改用插入排序。

5、对于一个每个元素都完全相同的一个序列来讲,快速排序也会退化到 O(n^2)。

三、选择排序

1. 简单选择排序

🍋 原理介绍:

秒懂算法4-选择排序_哔哩哔哩_bilibili

选择排序是一种简单的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

🍋 特点:

  1. 选择排序是一种不稳定的排序算法,因为它会改变相同关键字之间的相对位置。

  2. 选择排序不是很常用,因为它的时间复杂度较高。一般情况下,更快的排序算法,如快速排序、堆排序或归并排序,是更好的选择。

选择排序为什么不是稳定性排序呢?举个例子:数组 2、26、2、1、10,在对其进行第一遍循环的时候,会将第一个位置的2与后面的1进行交换。此时,就已经将两个2的相对前后位置改变了。因此选择排序不是稳定性排序算法。

🍋 代码示例:

/******************************************************
 * @description: ordinary select sort
 * @author: kashine
 * @version: 1.0 
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/27
*/

#include <iostream>
using namespace std;

void select_sort(int array[], int size)
{
    for(int i = 0; i < size - 1; i++)
    {
        int index = i;
        for(int j = i + 1; j < size; j++)
        {
            if(array[index] > array[j])
            {
                index = j;
            }
        }
        if(i != index)swap(array[i], array[index]);

    }
}

void display(int array[], int size)
{
    for(int i = 0; i < size; i++)
    {
        cout<< array[i]<< " ";
    }
    cout<<endl;
}

int main()
{
    int array[] = {5, 3, 6, 2, 10, 20, 15, 1, 3, 12};
    int size = sizeof(array) / sizeof(array[0]);

    display(array, size);

    select_sort(array, size);

    display(array, size);
    return 0;
}

🍋 算法复杂度:

最好时间复杂度:O(n2)

最坏时间复杂度:O(n2)

空间复杂度:O(1)

🍋 改进方案:

简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。

改进代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100;

void doubleSelectionSort(int a[], int n) {
    for (int i = 0; i < n / 2; i++) {
        int minIndex = i;
        int maxIndex = i;
        for (int j = i + 1; j < n - i; j++) {
            if (a[j] < a[minIndex]) {
                minIndex = j;
            }
            if (a[j] > a[maxIndex]) {
                maxIndex = j;
            }
        }
        if (minIndex != i) {
            swap(a[i], a[minIndex]);
        }
        if (maxIndex != n - i - 1) {
            swap(a[n - i - 1], a[maxIndex]);
        }
    }
}

int main() {
    int a[N];
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
    doubleSelectionSort(a, n);
    for (int i = 0; i < n; i++) {
        cout << a[i] << " ";
    }
    cout << endl;
    return 0;
}

2. 堆排序 

🍈 原理介绍:

堆排序动画演示,超简单易懂。亲测实用。_哔哩哔哩_bilibili

完全二叉树:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时候,称为完全二叉树。

// 完全二叉树
      1
    /   \
  2      3
 /  \      
4   5    

// 满二叉树
      1
    /   \
  2      3
 /  \   / \  
4   5  6   7

完全二叉树的顺序存储结构:从根起按照层次遍历的方式,也就是从上到下、从左到右存储结点元素。对于上面的完全二叉树,其顺序存储结构为:[1, 2, 3, 4, 5]。

堆:堆实质上是满足所有非终端结点的值不大于(或者不小于)其左右孩子结点值的完全二叉树。通常有两种类型:最大堆和最小堆,也称之为大根堆、小根堆。

// 大根堆
         100
        /   \
      50    75
     /  \  /  \
   25  40 20  10
   /
  5

// 小根堆
      5
     /  \
    10   20
   /  \  / \
  75  40 25  50
 /
100

堆排序:利用大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得当前无序序列中选择关键字最大(或最小)的记录变得简单。

🍈 特点:

  • 堆排序是一种不稳定的排序算法。
  • 堆排序使用了堆这种数据结构,因此可以很方便地实现优先队列这种数据结构。
  • 堆排序是一种原地排序算法,也就是说,它不需要额外的空间来存储临时数据。
  • 堆排序的实现较为复杂,不如其他算法实现简单。

🍈 代码示例:

/************************************************************
 * @description: heap sort 
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/28
*/

#include <iostream>
#include <vector>

using namespace std;

// 堆排序的辅助函数,用于将数组调整成大顶堆
// 注意:如果根节点i及其左右孩子满足大根堆定义的情况,但左右孩子所在子树不满足,此函数无法调整
// 也就是说,该函数仅适用于已经是大根堆,但是根节点被替换的情况!
// n是arr的长度,i为需要调整为堆的根节点(在数组的存储位置,相对于树的编号小1)
void heapify(int arr[], int n, int i) 
{
    // 先找到左右子节点的编号
    // 堆的存储一般用数组来实现。假如父节点的数组下标为i的话,那么其左右节点的下标分别为:
    // (2*i+1)和 (2*i+2)。如果孩子节点的下标为j的话,那么其父节点的下标为(j-1)/2
    int left = 2 * i + 1;// 因为下标从0开始,树的编号从1开始,因此这里都加上了1
    int right = 2 * i + 2;
    int largest = i; // 先让最大值等于根节点

    // 如果左子节点比根节点大,那么最大值变成左子节点
    if (left < n && arr[left] > arr[largest]) // left < n是为了防止叶子结点继续往下递归
    {
        largest = left;
    }

    // 如果右子节点比最大值还大,那么最大值变成右子节点
    if (right < n && arr[right] > arr[largest]) // right < n是为了防止叶子结点继续往下递归
    {
        largest = right;
    }

    // 如果最大值不是根节点,说明根节点不是最大值,需要交换
    if (largest != i) 
    {
        swap(arr[i], arr[largest]);

        // 交换后可能会破坏之前构建的堆,所以要再次调用 heapify 函数
        heapify(arr, n, largest);// largest与那个孩子交换,那么那个孩子所在子树的堆性质可能被破坏
    }
    // 如果largest是最大值,那么不会进行左右子树递归,因此该函数
    // 只是在根节点及其左右孩子违反大顶堆定义的之后才会进行调整
}

// 堆排序的主函数
void heapSort(int arr[], int n) 
{
    // 先将数组构建成大顶堆
    // 对于只有一个结点的树必然是堆,在完全二叉树中,所有序号大于⌊n/2⌋的结点都是叶子节点,
    // 叶子结点左右子树为空,显然以这些节点为根的子树均已是堆。这样只需要利用筛选法,
    // 从最后一个分支结点⌊n/2⌋开始,依次将序号为⌊n/2⌋、⌊n/2 -1⌋、⌊n/2 -2⌋...1的结点作为
    // 根的子树都调整为堆即可。

    // 注意:此处一定要从第一个不是叶子节点的结点开始,向根节点(存储下标小)的方向建立大根堆
    // 因为叶子节点已经是堆,上面的堆调整程序,仅适用于添加一个根节点将两个叶子结点作为左右子树
    // 建立大根堆,如果是从根节点开始,往叶子节点方向建立大根堆,则上面的程序不适用,也不合理
    for (int i = n / 2 - 1; i >= 0; i--) // ⌊n/2⌋为第一个不是叶子结点的结点,其存储下标为⌊n/2⌋-1
    {
        heapify(arr, n, i);
    }

    // 将堆顶的根节点与末尾的元素进行交换,并重新调整大顶堆
    // 首先从数组的最后一个位置,也就是完全二叉树的编号最大的结点开始,选出最大的放到该位置
    for (int i = n - 1; i >= 0; i--) // 每次选出一个最值,排序需要n次
    {
        swap(arr[0], arr[i]);
        heapify(arr, i, 0);
    }
}

int main() {
    // 根据完全二叉树的线性存储结构,arr[0]为根节点
    int arr[] = {12, 11, 13, 5, 6, 7};
    int n = sizeof(arr) / sizeof(arr[0]);

    heapSort(arr, n);

    for (int i : arr) 
    {
    cout << i << " ";
    }
    cout << endl;
    return 0;
}

🍈 算法复杂度:

参考堆排序的时间复杂度_redden333的博客-CSDN博客_堆排序时间复杂度

最好时间复杂度:O(nlog2n)

最坏时间复杂度:O(nlog2n)

空间复杂度:O(1)

🍈 改进方案:

  1. 使用优化的堆数据结构,例如左偏树或斜堆。这些数据结构在建立堆和重新构建堆时的时间复杂度更低,可以让堆排序的总时间复杂度更低。

  2. 使用计数排序或桶排序来对小范围数字进行排序,再将它们与堆排序结合起来。这样可以利用计数排序或桶排序的时间复杂度优势,同时仍然保留堆排序的稳定性。

四、归并排序

🥝 原理介绍:

归并排序就是将两个或两个以上的有序表合并成一个有序表的过程。将两个有序表合并成一个有序表的过程称为2-路归并。

🥝 特点:

  1. 稳定性:归并排序是一种稳定的排序算法,它保证相同的元素在排序后相对位置不变。

  2. 效率:归并排序是一种较快的排序算法,在排序大型数据时,它的效率很高。

  3. 适用范围:归并排序适用于排序数据规模较大的情况,因为它的常数较小,在排序大型数据时,它的效率很高。

🥝 代码示例:

递归代码:

/****************************************************************
 * @description: merge sort
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/29
****************************************************************/
#include <iostream>
#include <vector>

using namespace std;

void merge(vector<int>& array, int start, int mid, int end)
{
    // [start, mid]左侧有序, [mid + 1, end]右侧有序
    int n1 = mid - start + 1;// 左侧有序
    int n2 = end - mid;// 右侧有序

    // 将左右侧有序部分分别存储到temp1,temp2中
    vector<int> temp1, temp2;
    int i = 0, j = 0;
    while(i < n1)temp1.emplace_back(array[start + i++]);
    while(j < n2)temp2.emplace_back(array[mid + 1 + j++]);

    // 将temp1,temp2按顺序归并到array中
    i = 0; j = 0;
    int k = start;// 注意:k不能等于0!!!
    while(i < n1 && j < n2)
    {
        if(temp1[i] < temp2[j])
        {
            array[k++] = temp1[i++];
        }
        else
        {
            array[k++] = temp2[j++];
        }
    }

    // 将剩余元素拷贝到array中
    while(i < n1)array[k++] = temp1[i++];
    while(j < n2)array[k++] = temp2[j++];

}

// 归并排序
// start为排序起始下标,end为排序终止下标
// 二路归并实现排序,分治法(左、右)
void merge_sort(vector<int>& array, int start, int end)
{
    if(start < end)// 如果start大于或者等于end,不进行处理,即当数组只有一个元素不做处理
    {
        int mid = (start + end) / 2;
        merge_sort(array, start, mid);// T(n/2)
        merge_sort(array, mid + 1, end);// T(n/2)
        merge(array, start, mid, end);// O(n)
    }
}

int main()
{
    vector<int> array{5, 2, 4, 6, 1, 3};

    merge_sort(array, 0, array.size() - 1);
    for(auto& it : array)
    {
        cout<< it << " ";
    }
    cout<<endl;
    return 0;
}

非递归代码:

/****************************************************************
 * @description: merge sort
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/29
****************************************************************/
#include <iostream>
#include <vector>

using namespace std;

void merge(vector<int>& array, int start, int mid, int end)
{
    // [start, mid]左侧有序, [mid + 1, end]右侧有序
    int n1 = mid - start + 1;// 左侧有序
    int n2 = end - mid;// 右侧有序

    // 将左右侧有序部分分别存储到temp1,temp2中
    vector<int> temp1, temp2;
    int i = 0, j = 0;
    while(i < n1)temp1.emplace_back(array[start + i++]);
    while(j < n2)temp2.emplace_back(array[mid + 1 + j++]);

    // 将temp1,temp2按顺序归并到array中
    i = 0; j = 0;
    int k = start;// 注意:k不能等于0!!!
    while(i < n1 && j < n2)
    {
        if(temp1[i] < temp2[j])
        {
            array[k++] = temp1[i++];
        }
        else
        {
            array[k++] = temp2[j++];
        }
    }

    // 将剩余元素拷贝到array中
    while(i < n1)array[k++] = temp1[i++];
    while(j < n2)array[k++] = temp2[j++];

}


void mergeSort(vector<int>& arr) {
    int n = arr.size();

    // curr_size代表当前有序序列个数;,2*curr_size表示合并后的有序序列长度
    // 对于left来讲,总是从0开始,每次在curr_size的基础上*2,left_start< n - 1限制left范围
    // right要指向下一个left的左侧一个元素,left_start + 2 * curr_size - 1,但不超过n - 1
    for (int curr_size = 1; curr_size <= n - 1; curr_size = 2 * curr_size) // 控制合并前有序序列长度
    {
        for (int left_start = 0; left_start < n - 1; left_start += 2 * curr_size) // 控制左端点
        {   
            int right_end = std::min(left_start + 2 * curr_size - 1, n - 1);// 指向下一个left左侧一个元素,因此-1

            // [start, mid]左侧有序, [mid + 1, end]右侧有序
            int mid = left_start + curr_size - 1;// left加上长度-1,刚好指向第一个有序序列最后一个元素
            // int mid = (left_start + right_end) / 2;
            // int mid = left_start + (right_end - left_start) / 2;// 和上面的表述完全相同
            merge(arr, left_start, mid, right_end);
            cout<< left_start << " "<< mid << " "<< right_end<< endl;
        }
    }
}

int main() 
{
    vector<int> arr = {38, 27, 43, 3, 9, 82, 10, 1};

    mergeSort(arr);

    for (int i = 0; i < arr.size(); i++)
        std::cout << arr[i] << " ";
    std::cout << std::endl;

    return 0;
}

🥝 算法复杂度:

合并复杂度O(n),分治复杂度:T(n) = 2T(n/2) + O(n),类似于快速排序每一次取到的元素都刚好平分整个数组的情况。详细推导见:快速排序时间复杂度分析 - 知乎排序算法之 快速排序 及其时间复杂度和空间复杂度

时间复杂度:O(nlog2n)

时间复杂度:O(nlog2n)

空间复杂度:O(n)

因为不管元素在什么情况下都要做这些步骤,所以花销的时间是不变的,所以该算法的最优时间复杂度和最差时间复杂度及平均时间复杂度都是一样的为:O( nlogn )。

详见排序算法之 归并排序 及其时间复杂度和空间复杂度

🥝 改进方案:

归并排序的时间复杂度为O(nlgn),一般来讲,基于从单个记录开始两两归并的排序并不是特别提倡,一 种比较常用的改进就是结合插入排序,即先利用插入排序获得较长的有序子序列,然后再两两归并(改进后的归并仍是稳定的,因为插入排序是稳定的)。

五、非比较排序

1. 计数排序

🌽 原理介绍:

计数排序是一种非比较型排序算法,它的原理是对于每个输入的数,确定小于它的数的个数,然后把它放在对应的位置上。因此,计数排序的时间复杂度为 O(n),适用于数据范围较小的情况。

计数排序的基本步骤如下:

  1. 找到数组中的最大值和最小值。
  2. 初始化计数数组,记录每个数值出现的次数。
  3. 累加计数数组,记录小于等于每个数值的数的个数。(前缀和计算,建议先看基数排序代码注释)
  4. 从后往前扫描数组,将每个数值放在它在结果数组中的正确位置上。

 举例: 对数组 [10, 9, 5, 1, 3, 7] 进行计数排序的过程:

  1. 找到数组中的最大值和最小值。在这个例子中,最大值是 10,最小值是 1。

  2. 初始化计数数组,记录每个数值出现的次数。计数数组的大小为 maxValue - minValue + 1 = 10 - 1 + 1 = 10,初始化后的计数数组为 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

  3. 扫描一遍数组,记录每个数值出现的次数。最终的计数数组为 [1, 1, 0, 1, 0, 1, 1, 0, 1, 1]

  4. 累加计数数组,记录小于等于每个数值的数的个数。最终的计数数组为 [1, 2, 2, 3, 3, 4, 5, 5, 6, 7](此处为前缀和计算)

  5. 从后往前扫描数组,将每个数值放在它在结果数组中的正确位置上。最终得到的结果数组为 [1, 3, 5, 7, 9, 10](这里不好理解,详见基数排序代码注释,同此处)

所以,计数排序对数组 [10, 9, 5, 1, 3, 7] 进行排序的结果为 [1, 3, 5, 7, 9, 10]

🌽 特点:

  1. 时间复杂度低:计数排序的时间复杂度为 O(n),是一种线性时间复杂度的排序算法,比较快速。

  2. 稳定性好:计数排序是一种稳定的排序算法,也就是说,如果两个数的大小相同,那么它们在排序后的相对位置不会发生变化。

  3. 数据范围较小:计数排序适用于数据范围较小的情况,因为它需要开辟一个计数数组,大小为数据范围,如果数据范围较大,那么开辟的计数数组会很大,占用较多的空间。

  4. 不适用于大规模数据:由于计数排序需要开辟一个计数数组,如果数据规模较大,那么开辟的计数数组会很大,占用较多的空间,不适用于大规模数据的排序。

🌽 代码示例:

/************************************************************
 * @description: heap sort 
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/31
*************************************************************/

#include <iostream>
#include <vector>

using namespace std;

// 计数排序函数,arr 是待排序数组,result 是排序后的结果数组
void count_sort(const vector<int>& arr, vector<int>& result) {
  // 找到数组中的最大值和最小值
  int minValue = arr[0];
  int maxValue = arr[0];
  for (int i = 1; i < arr.size(); i++) {
    if (arr[i] < minValue) {
      minValue = arr[i];
    } else if (arr[i] > maxValue) {
      maxValue = arr[i];
    }
  }

  // 初始化计数数组,记录每个数值出现的次数
  vector<int> count(maxValue - minValue + 1);
  for (int i = 0; i < arr.size(); i++) {
    count[arr[i] - minValue]++;
  }

  // 累加计数数组,记录小于等于每个数值的数的个数
  for (int i = 1; i < count.size(); i++) {
    count[i] += count[i - 1];
  }

  // 从后往前扫描数组,将每个数值放在它在结果数组中的正确位置上
  for (int i = arr.size() - 1; i >= 0; i--) {
    result[count[arr[i] - minValue] - 1] = arr[i];
    count[arr[i] - minValue]--;
  }
}

void display(vector<int> arr)
{
    for(auto& it : arr)
    {
        cout << it<< " ";
    }
    cout<<endl;
}

int main() {
    vector<int> arr = {3, 6, 5, 2, 1, 4};
    vector<int> result(arr.size());

    display(arr);

    count_sort(arr, result);

    display(result);

    return 0;
}

🌽 算法复杂度:

最好时间复杂度:O(n)

最坏时间复杂度:O(n)

空间复杂度:O(n)

2. 桶排序

🥦 原理介绍:

它的原理是将数组分到有限数量的桶子里(分组), 对于每个桶,我们再利用其他排序算法将桶内的数据进行排序(组内排序),最后将桶子内的数据按顺序依次取出(输出),得到有序的结果数组。

桶排序的基本步骤如下:

  1. 设置一个定量的数组当作空桶,对要排序的数值划分桶区间,遍历数组,将数值放到对应的桶子中。

  2. 对每个不是空的桶子进行排序,可以使用其他排序算法或者以递归方式使用桶排序。

  3. 从不是空的桶子里按照顺序取出数据,组成有序的结果数组。

参考:桶排序算法 这个例子非常形象了

🥦 特点:

  1. 低时间复杂度:取决于桶内排序算法。
  2. 低空间复杂度:需要开辟桶数组。
  3. 稳定:大小相同的数在排序后相对位置不会发生变化。
  4. 适用于数据范围较小的情况。
  5. 不适用于大规模数据的排序。

🥦 代码示例:

/************************************************************
 * @description: heap sort 
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/31
*************************************************************/
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

// 桶排序
void bucket_sort(vector<int>& arr) 
{
    // 计算数组中的最大值和最小值
    int maxVal = *max_element(arr.begin(), arr.end());
    int minVal = *min_element(arr.begin(), arr.end());

    // 开辟桶数组
    vector<int> bucket(maxVal - minVal + 1);

    // 将数组中的元素放到对应的桶子中
    // 这个桶比较特殊,同一个桶内只存在相等的元素
    // 其实这个就是计数排序的“桶”
    for (int i = 0; i < arr.size(); i++) 
    {
        bucket[arr[i] - minVal]++;
    }

    // 遍历桶数组,将桶中的数据按顺序依次取出,得到有序的结果数组
    int k = 0;
    for (int i = 0; i < bucket.size(); i++)
     {
        while (bucket[i] > 0) // 表示bucket[i]中存放进去了数字
        {
            arr[k++] = i + minVal;
            bucket[i]--;
        }
    }
}

void display(vector<int> arr)
{
    for (int i = 0; i < arr.size(); i++) 
    {
        cout << arr[i] << " ";
    }
    cout << endl;
}

int main() {
    vector<int> arr = {10, 9, 5, 1, 3, 7};

    display(arr);
    bucket_sort(arr);
    display(arr);

    return 0;
}

🥦 算法复杂度:

最好时间复杂度:O(n + k)

最坏时间复杂度:O(n + k)

空间复杂度:O(n)

3. 基数排序

🥔 原理介绍:

[排序算法] 基数排序 (C++) - Amαdeus - 博客园

前述的各类排序方法都是建立在关键字比较的基础上,而基数排序是一种非比较型整数排序算法。它的基本思想是将整数按位数切割成不同的数字,然后按每个位数分别比较。

例如,将数字按个位、十位、百位...分别比较,基数排序有两种方法:

  • MSD:从低位到高位进行排序。这样从最低位开始排序时,较小的数就先出现在桶里。

  • LSD:从高位到低位进行排序。这样从最高位开始排序时,较大的数就先出现在桶里。

基数排序算法适用于对多个整数或者多个字符串进行升序或降序排序。 

一个整数由多个数字组成,例如 123 由 1、2、3 这 3 个数字组成;一个字符串由多个字符组成,例如 "lisi" 由 "l"、"i"、"s"、"i" 这 4 个字符组成。基数排序算法的实现思路是:对于待排序序列中的各个元素,依次比较它们包含的各个数字或字符,根据比较结果调整各个元素的位置,最终就可以得到一个有序序列。

 详见:基数排序算法

🥔 特点:

  1. 非比较型排序算法:基数排序不通过比较来决定元素间的相对次序,而是通过对数据的每位进行计数来确定每个数字的位置。

  2. 稳定性:基数排序是稳定的排序算法,也就是说,在基数排序过程中,相同大小的数字的相对位置不会发生变化。

  3. 时间复杂度:基数排序的时间复杂度取决于数据范围和数据的位数。当数据范围很小,且数据的位数较少时,基数排序是一种较快的排序算法。

  4. 空间复杂度:基数排序需要较多的空间来存储桶和计数器,因此空间复杂度较高。

  5. 适用范围:基数排序适用于排序非负整数,并且在数据范围不大,且数据的位数较少的情况下效率较高。

🥔 代码示例:

数组数字排序:

/************************************************************
 * @description: heap sort 
 * @author: kashine
 * @version: 1.0
 * @mail: likaiqinchina@gmail.com
 * @date: 2022/12/31
*************************************************************/

#include <iostream>
using namespace std;

// 对于数字来讲0~9,MAX = 10,对于字符串来讲a~z,MAX = 26
#define MAX 10 //基数

//计数排序算法
// array:数组
// place:数位,如1 10 100 1000等
// 按照某一位对数组array进行排序
void counting_sort(int array[], int place, int size) 
{
    int output[size];
    // 初始化一个数组,记录各个元素的出现次数
    int count[MAX] = { 0 };// count[] 中每一项对应一个桶,统计每一位数字出现的次数

    // 统计各个元素出现的次数
    for (int i = 0; i < size; i++) count[(array[i] / place) % 10]++;

    // if(place == 1)
    // {
    //     cout<< "个位各个数字出现次数:";
    //     for(int i = 0; i < 10; i++)cout<< count[i]<< " ";
    //     cout<<endl;
    // }

    // 累加count 数组中的出现次数
    // 真的是妙!
    // 某个数字,比如2,个位数为2的可能有多个,个位数比2小的可能同样有多个
    // 那么个位数为2的元素放在哪里呢?前面肯定是个位数小于2的,个数为:个位数小于2的数字的个数和,
    // 个位数同样为2的放在其后面,顺序不限
    // 上面的count中从下标0~9,代表着各个个位数字出现次数,按照下面方式累加
    // cout[2]变为第二个元素应该放的位置,也就是2出现次数加上1、0出现次数
    // 假如存在2个 个位数为2的数字,其前面只有一个 个位数为0的数字,
    // 那么cout[0] = 1; cout[1] = 0; cout[2] = 2;
    // 经过累加之后cout[0] = 1; cout[1] = [1]; cout[2] = 3;
    // 也就是说个位数为0的应该放在第一个位置,下标-1,即下标为0的位置
    // 由于不存在个位数为1的数字,所以count[1]不会被访问,不影响前后数字存放
    // 个位数为2的数字应该放在第三个位置,下标为3 - 1 = 2
    // 由于存在两个个位数为2的数字,在存放完一个个位数为2的数字之后,将count[2] - 1,
    // 下次再遇到个位数为2的数字,存放在前一个位置,也就是下标为1的位置,再次cout[2] - 1,
    // 由于不存在第三个个位数为2的数字,所以cout[2]不会被继续访问递减,
    // count[2]最终等于个位数小于2的数字个数
    for (int i = 1; i < 10; i++) count[i] += count[i - 1];// 前缀和计算https://blog.csdn.net/qq_45914558/article/details/107385862

    // if(place == 1)
    // {
    //     cout<< "从小到大累加后:";
    //     for(int i = 0; i < 10; i++)cout<< count[i]<< " ";
    //     cout<<endl;
    // }

    // 根据count 数组中的信息,找到各个元素排序后所在位置,存储在output 数组中
    for (int i = size - 1; i >= 0; i--)
    {
        output[count[(array[i] / place) % 10] - 1] = array[i];
        count[(array[i] / place) % 10]--;
    }
    // 将output 数组中的数据原封不动地拷贝到 array 数组中
    for (int i = 0; i < size; i++) array[i] = output[i];
}

// 找到整个序列中的最大值
int get_max(int array[], int size) 
{
    int i, max = array[0];
    for (i = 1; i < size; i++)
        if (array[i] > max)
            max = array[i];
    return max;
}

// 基数排序算法
void radix_sort(int array[], int size) {
    // 找到序列中的最大值
    int place, max = get_max(array, size);
    // 根据最大值具有的位数,从低位依次调用计数排序算法
    for (place = 1; max / place > 0; place *= 10)
        counting_sort(array, place, size);
}

// 输出 array 数组中的数据
void display_array(int array[], int size) {
    int i;
    for (i = 0; i < size; ++i) {
        cout<< array[i]<< " ";
    }
    cout<< endl;
}

int main() {
    int array[12] = { 121, 432, 564, 23, 1, 10, 45, 788, 2, 6, 23, 19};
    int size = sizeof(array) / sizeof(array[0]);
    display_array(array, size);// 前
    radix_sort(array, size);
    display_array(array, size);// 后

    return 0;
}

🥔 算法复杂度:

最坏时间复杂度:O(k*n)

最好时间复杂度:O(k*n)

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

k为程序中place变量大小,在对数组排序的时候为10,对小写字符串排序的时候为26。


  • 7
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kashine

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值