九大经典排序算法(王道考研排序算法整理)

简介: 九大经典排序算法(王道考研排序算法整理)

6. 排序


算法稳定性:若待排序表有两个元素 R i 和 R j ,其对应的关键字相同即 k e y i = k e y j

,且排序前 R i 在 R j 前面。


若排序后 R i 仍然在 R j前面,则是稳定的。

反之亦然 👊


6.1 直接插入排序

初始 L [ 1 ]  可以视为已经排好的子序列,然后依次将 L [ 2 ] − l [ n ] 插入已经排好的序列,共执行 n − 1 次操作。每次操作的具体步骤为从后往前依次和子序列元素比较,如果小于该元素往后移位,最后将自己插入适当的位置。

// 直接插入排序
void DirectInsertSort(ElementType A[], int n)
{
    int i, j;
    for (i = 2; i <= n; i++)
    {
        // 看是否比序列大,如果大就不用移动位置,反之要进入循环移动位置
        if (A[i] < A[i - 1])
        {
            // 放置哨兵
            A[0] = A[i];
            for (j = i - 1; A[j] > A[0]; j--)
            {
                // 向后移位
                A[j + 1] = A[j];
            }
            // 将哨兵里元素放到属于他的位置
            A[j + 1] = A[0];
        }
    }
}


空间复杂度:O ( 1 )


时间复杂度:


最好情况:初始有序,每次插入元素就比较一次而不用移动元素,时间复杂度 O ( n ) O(n)O(n)

最坏情况:初始逆序,时间复杂度 O ( n 2 )

平均时间复杂度 O ( n 2 )


6.2 折半插入排序

查询元素位置用二分查找实现,但与二分查找不同,二分查找是查到元素的位置,但这里其实是通过左右比较,找到一个适当的位置,所以最后会以 h i g h = l o w − 1 high=low-1high=low−1 结束,所以 h i g h highhigh 指向元素就是应该插入元素的前一个元素。然后再统一向后移动元素。

// 折半插入排序
void BinaryInsertSort(ElementType A[], int n)
{
    int i, j, mid, low, high;
    for (int i = 2; i <= n; i++)
    {
        A[0] = A[i];
        low = 1, high = i - 1;
        // 二分查找定位置
        while (low <= high)
        {
            mid = (low + high) / 2;
            if (A[mid] > A[0])
            {
                high = mid - 1;
            }
            else
            {
                low = mid + 1;
            }
        }
        // 最后会以high=low-1结束,所以high指向元素就是应该插入元素的前一个元素
        // 那么high+1表示的就是应该插入元素的位置
        for (j = i - 1; j >= high + 1; j--)
        {
            A[j + 1] = A[j];
        }
        A[high + 1] = A[0];
    }
}


空间复杂度:O ( 1 )

时间复杂度:


仅减少了比较元素的次数,未改变移动次数,因此平均时间复杂度还是 O ( n 2 )


6.3 希尔排序

将相隔某一增量的元素组成子表,对每个子表直接插入排序。这个某一增量的取值一般为 d 1 = n / 2 ,d_{i+1}=d_i/2,到最后一个增量为1为止。相当于在元素基本有序时,对全体进行一次直接插入排序。

// 希尔排序
void ShellSort(ElementType A[], int n)
{
    // 记录步长
    for (int dk = n / 2; dk >= 1; dk = dk / 2)
    {
        for (int i = dk + 1; i <= n; i++)
        {
            if (A[i] < A[i - dk])
            {
                A[0] = A[i];
                int j;
                for (j = i - dk; A[0] < A[j] && j > 0; j -= dk)
                {
                    A[j + dk] = A[j];
                }
                A[j + dk] = A[0];
            }
        }
    }
}


空间复杂度:O ( 1 )


时间复杂度:


分析十分复杂,记住最优情况下约为 O ( n 1.3 ) ,最坏情况下 O ( n 2 )


6.4 冒泡排序

从后往前两两比较相邻元素的值,若为逆序则交换。这是第一趟冒泡。结果是最小的元素到了第一个位置。下一趟冒泡,已经确定的最小元素不用参与,最多重复 n − 1 n-1n−1 趟冒泡就能把所有元素排好序。


// 冒泡排序
void BubbleSort(ElementType A[], int n)
{
    for (int i = 0; i < n - 1; ++i)
    {
        // 判断本趟冒泡是否发生交换的标志
        bool flag = false;
        for (int j = n - 1; j > i; j--)
        {
            if (A[j - 1] > A[j])
            {
                swap(A[j - 1], A[j]);
                flag = true;
            }
        }
        if (!flag)
        {
            break;
        }
    }
}


空间复杂度:O ( 1 )

时间复杂度:


最好情况:初始有序,只进行一次冒泡,时间复杂度 O ( n )

最坏情况:初始逆序,需要 n − 1次排序,时间复杂度 O ( n 2 )

平均时间复杂度 O ( n 2 )


6.5 ⭐️快速排序

任取一元素 p i v o t  作为枢轴,一趟排序分成2部分,p i v o t 左边所有元素小于它,p i v o t 右边所有元素大于它。然后分别递归的对两个子表重复上述过程。直到每部分只有一个元素。

// 快速排序
void QuickSort(ElementType A[], int low, int high)
{
    // 当子序列只剩一个元素时,pivot-1=low,这时high=low,函数结束
    if (low < high)
    {
        // 划分得到枢轴点位置
        int pivotpos = Partition(A, low, high);
        QuickSort(A, low, pivotpos - 1);
        QuickSort(A, pivotpos + 1, high);
    }
}
// 快速排序划分操作
int Partition(ElementType A[], int low, int high)
{
    // 选取第一个元素为枢轴点
    ElementType pivot = A[low];
    while (low < high)
    {
        // 循环找从右向左小于pivot的元素
        while (A[high] >= pivot && high > low)
        {
            high--;
        }
        // 找到该元素将该元素移到左端
        A[low] = A[high];
        while (A[low] <= pivot && high > low)
        {
            low++;
        }
        // 将比枢轴大的元素移到右端
        A[high] = A[low];
    }
    // 此时low=high,存放枢轴元素的值
    A[low] = pivot;
    return low;
}


空间复杂度:O ( l o g 2 n )


时间复杂度:O ( l o g 2 n )


6.6 简单选择排序

就每次都选择序列中最小的元素,重复 n − 1 n-1n−1 趟。

// 简单选择排序
void SelectSort(ElementType A[], int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        int j, min = i;
        for (j = i + 1; j < n; j++)
        {
            if (A[j] < A[min])
            {
                min = j;
            }
        }
        if (min != i)
        {
            swap(A[i], A[min]);
        }
    }
}


空间复杂度:O ( 1 )


时间复杂度:O ( n 2 )


6.7 ⭐️堆排序

根据堆的特性,最大堆堆顶元素就是最大值,输出堆顶元素后,将堆底元素放到堆顶,此时堆的性质被破坏,需要重新调整。


❓ 待解决问题?


1️⃣ 如何将无序序列构造成初始堆?

2️⃣ 输出堆顶元素后,如何调整堆?

对于 n 个结点的完全二叉树,最后一个结点的父节点是 n / 2,所以从 n / 2 为根结点的子树开始向前筛选,使子树成为堆。筛选方法:看结点是否大于左右子结点值,如小于,与左右子结点中较大者交换。交换后可能会破环下一级堆,于是继续调整。

// 堆排序
void HeapSort(ElementType A[], int len)
{
    BuildMaxHeap(A, len);
    for (int i = len; i > 1; i--)
    {
        // 将根结点与末尾结点交换位置,那么最大元素就被移到数组最后面
        swap(A[i], A[1]);
        // 同时调整堆的时候只对前面i-1元素进行,这样影响不到后面的已排好元素
        HeadAdjust(A, 1, i - 1);
    }
}
// 建堆
void BuildMaxHeap(ElementType A[], int len)
{
    for (int i = len / 2; i > 0; i--)
    {
        HeadAdjust(A, i, len);
    }
}
// 调整堆
void HeadAdjust(ElementType A[], int k, int len)
{
    // A[0]暂时存放子树的根结点
    A[0] = A[k];
    for (int i = 2 * k; i <= len; i *= 2)
    {
        // i如果等于len则没有右子树,也就没有比较左右子树的必要了
        if (i < len && A[i] < A[i + 1])
        {
            i++;
        }
        if (A[i] <= A[0])
        {
            break;
        }
        else
        {
            // 将A[i]复制到父节点
            A[k] = A[i];
            // 修改k,用以定位子树根结点应该放置的位置
            k = i;
        }
    }
    A[k] = A[0];
}


空间复杂度:O ( 1 )


时间复杂度:O ( n l o g 2 n )


6.8 ⭐️归并排序

待排序表有 n  个记录,就看成 n 个有序的子表,每个子表长度为1,然后两两归并,得到 n / 2 个长度为2或者1个有序表,继续两两归并,直到合并成一个长度为 n 的有序表为止。

// 归并排序
void MergeSort(ElementType A[], int low, int high)
{
    // 划分子序列,分到每个子序列长度为1
    if (low < high)
    {
        int mid = (low + high) / 2;
        MergeSort(A, low, mid);
        MergeSort(A, mid + 1, high);
        Merge(A, low, mid, high);
    }
}
// 归并操作
void Merge(ElementType A[], int low, int mid, int high)
{
    int i = low;
    int j = mid + 1;
    int k;
    // 把子序列所有元素复制到B数组
    for (int k = low; k <= high; k++)
    {
        B[k] = A[k];
    }
    for (k = low; i <= mid && j <= high; k++)
    {
        // 两个子序列相互比较,当一个序列的元素被安置完后
        // 另外一个序列的剩余元素肯定是大于且有序的,直接顺序插入
        if (B[i] < B[j])
        {
            A[k] = B[i++];
        }
        else
        {
            A[k] = B[j++];
        }
    }
    // 如果第一个表没有检测完,复制
    while (i <= mid)
    {
        A[k++] = B[i++];
    }
    // 同理
    while (j <= high)
    {
        A[k++] = B[j++];
    }
    // 注:只有一个while会执行
}


空间复杂度:O ( n )


时间复杂度:O ( n l o g 2 n )


6.9 基数排序

以关键字是1000一下的整数为例,基数 r = 10,每个关键字由3个子关键字组成:k 1 K 2 K 3  ,分别代表百位,十位和个位,从次位开始(个位)依次进行操作。


次位优先基数排序过程:


分配


创建 r = 10 r=10r=10 个空桶,按照每个元素的当前子关键字数值放置到不同的桶中


收集


把每个桶的结点首尾相连,形成一个新的线性表

#include <iostream>
using namespace std;
struct node
{
    int data;
    node *next;
};
// 获得一个数的各个位上的数字
int GetDigit(int X, int D)
{
    // 默认次位D=1, 主位D<=MaxDigit
    int d, i;
    for (i = 1; i <= D; i++)
    {
        d = X % 10;
        X /= 10;
    }
    return d;
}
int main()
{
    // 基数
    int radix = 10;
    // 建立桶
    node *bucket[radix];
    // 初始化待排序数组
    int A[10] = {278, 109, 63, 930, 589, 184, 505, 269, 8, 83};
    node *list = NULL;
    node *p;
    for (int i = 0; i < 10; i++)
    {
        node *tmp = new node;
        tmp->data = A[i];
        tmp->next = NULL;
        if (list == NULL)
        {
            list = tmp;
            p = list;
        }
        else
        {
            p->next = tmp;
            p = p->next;
        }
    }
    // 开始排序,对每一位进行处理
    for (int i = 1; i <= 3; i++)
    {
        // 清空桶
        for (int i = 0; i < radix; i++)
        {
            bucket[i] = NULL;
        }
        p = list;
        node *tmp;
        // 分配
        while (p)
        {
            tmp = p;
            p = p->next;
            int k = GetDigit(tmp->data, i);
            if (bucket[k] == NULL)
            {
                bucket[k] = tmp;
            }
            else
            {
                node *q = bucket[k];
                while (q->next)
                {
                    q = q->next;
                }
                q->next = tmp;
            }
            tmp->next = NULL;
        }
        // 收集
        list = NULL;
        for (int j = 0; j < radix; j++)
        {
            if (bucket[j] == NULL)
            {
                continue;
            }
            // 链表首指针定位
            if (bucket[j] && list == NULL)
            {
                list = bucket[j];
                p = list;
                continue;
            }
            // 此时p任指向上一个存在的桶
            while (p->next)
            {
                p = p->next;
            }
            // 如现在的桶存在,让上一个桶的最后一个结点指过来
            p->next = bucket[j];
        }
        // 打印
        cout << i << " trip: ";
        p = list;
        while (p)
        {
            cout << p->data << " ";
            p = p->next;
        }
        cout << endl;
    }
    system("pause");
    return 0;
}


r 为桶个数,d为子关键字个数(趟数),n 为元素个数


空间复杂度:O ( r )


时间复杂度:O ( d ( n + r ) )


6.10 ☪️总结

目录
相关文章
|
4月前
|
存储 算法 搜索推荐
【算法训练-排序算法 三】【排序应用】合并区间
【算法训练-排序算法 三】【排序应用】合并区间
48 0
|
2月前
|
存储 分布式计算 搜索推荐
【数据结构排序算法篇】----对十大经典算法的总结
【数据结构排序算法篇】----对十大经典算法的总结
32 0
|
2月前
|
搜索推荐 算法 大数据
【数据结构入门精讲 | 第十篇】考研408排序算法专项练习(二)
【数据结构入门精讲 | 第十篇】考研408排序算法专项练习(二)
25 0
|
2月前
|
存储 搜索推荐 算法
【数据结构入门精讲 | 第九篇】考研408排序算法专项练习(一)
【数据结构入门精讲 | 第九篇】考研408排序算法专项练习(一)
36 0
|
3月前
|
机器学习/深度学习 存储 人工智能
算法05-排序算法
算法05-排序算法
|
3月前
|
搜索推荐 JavaScript 前端开发
用openAI写个js的排序算法(快速排序算法)
用openAI写个js的排序算法(快速排序算法)
19 0
|
3月前
|
算法 搜索推荐 Java
数据结构与算法面试:基于比较的排序算法时间复杂度最坏情况下是 O(nlogn),请问有没有更快的算法?(提示:计数排序、基数排序)
数据结构与算法面试:基于比较的排序算法时间复杂度最坏情况下是 O(nlogn),请问有没有更快的算法?(提示:计数排序、基数排序)
21 0
|
4月前
|
机器学习/深度学习 算法 搜索推荐
【算法训练-排序算法 一】【手撕排序】快速排序、堆排序、归并排序
【算法训练-排序算法 一】【手撕排序】快速排序、堆排序、归并排序
56 0
|
5月前
|
存储 算法 调度
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(下)
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)
|
10月前
|
存储 固态存储 程序员
考研计算机组成原理总结(5)
考研计算机组成原理总结(5)
297 0

热门文章

最新文章