C++实现排序算法

插入排序

基本思想

每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。即边插入边排序,保证子序列中随时都是排好序的。

基本操作

  • 在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
  • 起初,a[0]是长度为1的子序列。然后逐一将a[1]至a[n-1]插入到有序子序列中。
  • 在插入a[i]前,数组a的前半段(a[0]-a[i-1])是有序段,后半段(a[i]-a[n-1])是停留与输入次序的 “无序段”
  • 插入a[i]使a[0]-a[i-1]有序,也就是要为a[i]找到有序位置j(0 \le j \le i),将a[i]插入在a[j]的位置上。

插入排序的种类

  • 直接插入排序:顺序法定位插入位置。
  • 二分插入排序:二分法定位插入位置。
  • 希尔排序:缩小增量多遍插入排序。

 直接插入排序

  1. 复制插入元素,temp = a[i];
  2. 记录后移,查找插入位置。
  3. 插入到正确的位置。

直接插入排序代码实现

// 直接插入排序(0号位置设置为哨兵)
void directSort(int a[], int len)
{
    int i,j;
    for(i = 2; i < len; i++) // 0号位置为哨兵,1号位置默认有序,所以从2号位置开始。
    {
        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];
        }
    }   
} 

直接插入排序性能分析

实现直接插入排序的基本操作有两个:

  1. “比较”序列中两个关键字的大小;
  2. “移动”记录;

最好的情况:关键字在记录序列中已经顺序有序。那么只会执行比较操作,不会执行移动操作,并且比较次数为 n - 1 次;

最坏的情况:关键字在记录序列中逆序有序。每个元素都要与其前面所以元素进行比较,同时每个元素都要移动到最前面;所以比较的次数为:\sum\limits_{i = 2}^n {i = \frac{​{(n + 2)(n - 1)}}{2}},移动的次数为\sum\limits_{i = 2}^n {\(i+1\) = \frac{​{(n + 4)(n - 1)}}{2}}

平均的情况:比较次数:\sum\limits_{i = 1}^{n-1}\frac{i+1}{2} = \frac{1}{4}{(n+2)}{(n-1)}
                      移动次数:\sum\limits_{i = 1}^{n-1}\(\frac{i+1}{2} +1\)= \frac{1}{4}{(n+6)}{(n-1)}

复杂度结论

  • 原始数据越接近有序,排序速度越快;
  • 最坏的情况下(输入序列是逆有序的):Tw(n) = O(n^2)
  • 平均情况下,耗时差不多是最坏情况的一般:Te(n) = O(n^2)
  • 如果想提高查找速度
    • 减少元素的比较次数
    • 减少元素的移动次数
  • 空间复杂度:只需要一个哨兵的,O(1)

二分插入排序

 直接插入排序在查找插入位置时,是从后向前依次进行比较。但是我们已知在前半段是有序的,故在查找插入位置时可以使用二分查找法,由此诞生了二分插入排序。和直接插入排序的主要区别就是在查找插入位置的时候,一个使用顺序查找的方法,一个使用二分查找的方法。

二分插入排序代码实现

// 二分插入排序(折半插入排序)
void binInertSort(int a[], int len)
{
    int left, right, mid;
    for(int i = 2; i < len; i++)
    {
        a[0] = a[i]; // 将当前元素插入到哨兵位置
        left = 1, right = i - 1;
        while(left <= right) // while循环结束之后 right + 1 的位置即为要插入的元素位置
        {
            mid = (left + right) / 2;
            if(a[mid] > a[0])
            {
                right = mid - 1;
            }
            else
            {
                left = mid + 1;
            }
        }
        // 找到要插入的位置之后,将此位置之后的元素都向后移动一个位置
        for(int j = i; j != right; j--)
        {
            a[j] = a[j - 1];
        }
        a[right + 1] = a[0];
    }
}

二分插入排序性能分析

  • 二分查找比顺序查找快,所以二分插入排序的就平均性能来说比直接插入排序要快;
  • 它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。在插入第i 个对象时,需要经过 \left\lfloor\log _{2}{i}\right\rfloor + 1 次关键码比较,才能确定它应插入的位置;
    • 当 n 较大时,总关键码比较次数比直接插入排序的最坏情况要好的多,但比最好情况要差;
    • 在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比二分插入排序执行的关键码比较次数要少;
  • 二分插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列。
    • 减少了比较次数,但是没有减少移动次数;
    • 平均性能优于直接插入排序。
  • 时间复杂度仍然为O(n^2),空间复杂度为O(1)

希尔排序

基本思想

先将整个待排序记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的序列基本有序时,再对全体记录进行以此直接插入排序。因此希尔排序算法的做法为:缩小增量,多遍插入排序。

希尔排序思路

  1. 定义增量序列  D_k:D_M>D_{M-1}>\dots>D_1 = 1;
  2. 对每个 D_k 进行“D_k-间隔”插入排序  (k = M,M-1,\dots,1);

希尔排序特点

  • 一次移动,移动位置较大,跳跃式地接近排序后的最终位置;
  • 最后一次只需要少量移动;
  • 增量序列必须时递减的,最后一个必须是1;
  • 增量序列应该是互质的。

希尔排序代码实现

// 希尔排序
void shellInsertSort(int a[], int len_a, int dk)
{
    //对数组a进行一次增量为dk的shell排序,dk为步长因子。
    int i, j;
    for(i = dk + 1; i < len_a; i++)
    {
        if(a[i] < a[i - dk])
        {
            a[0] = a[i];     // 复制为哨兵~
            for(j = i - dk; a[j] > a[0]; j = j - dk)
            {
                a[j + dk] = a[j];         
            }
            a[j + dk] = a[0];
        }
    }
}

void shellInsertSort(int a[], int len_a, int delta[], int len_delta)
{
    // 按增量序列 delta[0] - delta[len_delta]对数组 a 作希尔排序
    for(int i = 0; i < len_delta; i++)
    {
        shellInsertSort(a, len_a, delta[i]);
    }
}

希尔排序性能分析

希尔排序的算法效率与增量序列的取值有关;

  • Hibbard增量序列
    • D_k =2^{k}-1 --------  相邻元素互质
    • 最坏情况:T_{worst}=O(n^{3/2})
    • 猜想:T_{avg}=O(n^{5/4})
  • 希尔排序算法是一种 不稳定 的排序算法;
    • 如何选取最佳的增量序列,目前尚未解决;
    • 最后一个增量值必须为1,其他增量值除1之外无公因子;
    • 不宜在链式存储结构上实现。
  • 时间复杂度是经验公式:O(n^1.25) \sim O(1.6n^{1.25}),空间复杂度为O(1)

交换排序

基本思想

两两比较,如果发生逆序则交换,知道所有记录都排好序为止。

常见的交换排序方法

  • 冒泡排序:O(n^2)
  • 快速排序:O(nlog_{2}n)

冒泡排序

相邻两个数两两比较,每轮选出一个最大或者最小的值,在下一轮中不再参与比较。

  • 优点:每轮结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;
  • 如何提高效率:一旦某一轮没有出现交换记录时,说明已经排好序了,就可以结束本算法了;

冒泡排序代码实现

// 冒泡排序
void bubbleSort(int a[], int len)
{
    bool flag = true; // 增加一个标志位,判断是否在某一轮没有发生交换,那么算法提前结束
    for(int i = 0; i < len - 1 && flag; i++)
    {
        flag = false;
        for(int j = 0; j < len - i - 1; j++)
        {
            if(a[j] > a[j + 1])
            {
                int temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
                flag = true;
            }

        }
    }
}

冒泡排序复杂度分析

  • 时间复杂度
    • 最好情况(正序):
      • 比较次数:n - 1
      • 移动次数:0
    • 最坏情况(逆序):
      • 比较次数:\sum_{i=1}^{n-1}{(n-i)}=\frac{n(n-1)}{2}
      • 移动次数:与比较次数相同。

冒泡排序的算法评价

  • 冒泡排序最好的时间复杂度是O(n)
  • 冒泡排序最坏的时间复杂度为O(n^2)
  • 冒泡排序平均的时间复杂度为O(n^2)
  • 冒泡排序算法中增加一个辅助空间 temp,空间复杂度为O(1)
  • 冒泡排序是稳定的

快速排序

改进的交换排序,基本思想:

  • 任取一个元素(如:第一个)为中心;
  • 所以比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表;
  • 对各子表重新选择中心元素并依此规则调整; // 递归思想
  • 直到每个子表的元素只剩一个;
  • 通过一轮排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字比均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序;

快速排序特点

  • 每一轮的子表的形成是采用从两头向中间交替式逼近法;
  • 由于每轮中对各子表的操作都相似,可采用递归算法;

快速排序代码实现

// 快速排序
int partition(int a[], int low, int high)
{
    a[0] = a[low];
    while(low < high)
    {
        while(low < high && a[high] > a[0])
        {
            high--;
        }
        a[low] = a[high];
        while(low < high && a[low] < a[0])
        {
            low++;
        }
        a[high] = a[low];
    }
    a[low] = a[0];
    return low;
}

void quickSort(int a[], int low, int high)
{
    if(low < high)
    {
        int pivotlocated = partition(a, low, high);
        quickSort(a, low, pivotlocated - 1);
        quickSort(a, pivotlocated + 1, high);
    }
}

快速排序性能分析

  • 时间复杂度
    • 可以证明,平均计算时间是O(nlog_2n),
      • quickSort():O(log_2n)
      • partition():O(n)
    • 实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。
  • 空间复杂度
    • 快速排序不是原地排序,由于程序中使用了递归,需要递归调用栈的支持,而栈的长度取决于递归调用的深度。(即使不用递归,也需要用用户栈);
    • 在平均情况下:需要O(\log n)的栈空间;
    • 最坏情况下:栈空间可达O(n)
  • 稳定性:快速排序是一种 不稳定 的排序方法。
  • 快速排序不适于对原本有序或基本有序的记录序列进行排序。

选择排序

基本思想

在待排序的数据中选出最大(小)的元素放在其最终的位置。

基本操作

  1. 首先通过 n - 1 次关键字比较,从 n 个记录中找出关键字最小的记录,将他与第一个记录交换。
  2. 再通过 n - 2 次比较,从剩余的 n - 1 个记录中找出关键字次小的记录,将它与第二个记录交换。
  3. 重复上述操作,共进行 n - 1 轮排序后,排序结束。

简单选择排序(直接选择排序)

代码实现

// 简单选择排序
void selectSort(int a[], int len)
{
    for(int i = 0; i < len - 1; i++)
    {
        int min_index = i;
        for(int j = i + 1; j < len; j++)
        {
            if(a[j] < a[min_index])
            {
                min_index = j;
            }
        }
        if(min_index != i)
        {
            int temp = a[i];
            a[i] = a[min_index];
            a[min_index] = temp;
        }
    }
}

简单选择排序性能分析

  • 时间复杂度
    • 记录移动次数
      • 最好情况:0
      • 最坏情况:3(n - 1)
    • 比较次数:无论待排序列处于什么状态,选择排序所需要进行的“比较”次数都相同:\sum_{i=1}^{n-1}(n-i)=\frac{n(n-1)}{2}
  • 算法稳定性:简答选择排序是不稳定排序
  • 空间复杂度:O(1), 只是在交换时需要一个临时的 temp 空间。

堆排序

堆的定义

若 n 个元素的序列 \{a_1,a_2,\dots,a_n\} 满足:\left\{ \begin{array}{l} a_i \le a_{2i}\\ a_i \le a_{2i+1} \end{array} \right.  或 \left\{ \begin{array}{l} a_i \ge a_{2i} \\ a_i \ge a_{2i+1} \end{array} \right., 则分别称该序列\{a_1,a_2,\dots,a_n\}小根堆大根堆

从堆的定义可以看出,堆实质是满足如下性质的完全二叉树:二叉树中的任一非叶子结点均小于(大于)他的孩子结点。

堆排序

若在输出堆顶的最小值(最大值)后,使得剩余 n - 1 个元素的序列重新又建成一个堆,则得到 n 个元素的次小值(次大值),如此反复,便能得到一个有序序列,这个过程称之为 堆排序。

  • 实现堆排序需要解决两个问题
    • 如何由一个无序序列建成一个堆?
    • 如何在输出堆顶元素后,调整剩余元素为一个新的堆?

如何在输出堆顶元素后,调整剩余元素为一个新的堆?

  • 小根堆(大根堆)
  1. 输出堆顶元素之后,以堆中最后一个元素替代之;

  2. 然后将根节点值与左、右子树的根节点值进行比较,并与其中小者(大者)进行交换;
  3. 重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子结点的调整过程为“筛选”;

如何由一个无序序列建成一个堆?

  • 显然,单结点的二叉树是堆;
  • 在完全二叉树中所有以叶子结点(序号 i >n/2)为根的子树是堆;
  • 这样我们只需要以此将序号为 \frac{n}{2},\frac{n}{2}-1,\dots,1 的结点为根的子树调整为堆即可

堆排序代码实现

// 堆排序
void heapAdjust(int a[], int s, int m)
{
    int temp = a[s];
    for(int j = 2*s+1; j <= m; j = 2*j+1) // 考虑到数组编号是从 0 开始的,所以这里根结点的左孩子编号稍有改动
    {
        if(j < m && a[j] < a[j+1])
        {
            j++;
        }
        if(temp >= a[j])
        {
            break;
        }
        a[s] = a[j];
        s = j;
    }
    a[s] = temp;
}

void heapSort(int a[], int len)
{
    for(int i = (len-2) / 2; i >=0; i--) // 同理,这里 parents 结点的编号也稍有改动
    {
        heapAdjust(a, i, len-1);
    }
    for(int i = len - 1; i > 0; i--)
    {
        int temp = a[0];
        a[0] = a[i];
        a[i] = temp;
        heapAdjust(a, 0, i - 1);
    }
}

归并排序

基本思想

将两个或两个以上的有序子序列“归并”成一个有序序列。

  • 在内部排序中,通常采用的是 2-路归并排序
    • 将两个位置相邻的有序子序列归R\[l\dots m\]R\[m+1 \dots n\]并为一个有序序列R\[l \dots n\].

归并排序算法分析

  • 时间效率:O(n\log _2n)
  • 空间效率:O(n),因为需要一个与原始序列同样大小的辅助序列,这正是此算法的缺点。(那我用链表是不是会好一点呢???)
  • 稳定性:稳定。

基数排序

基本思想

分配 + 收集,也叫桶排序箱排序:设置若干个箱子,将关键字为 k 的记录放入第 k 个箱子,然后再按序号将非空的连接。

数字是有范围的,均由 0 - 9 这是个数字组成,则只需设置十个箱子,相继按 个、十、百...  进行排序。

基数排序算法分析

  • 时间效率:O(k*(m+n))k:关键字个数;m:关键字取值范围为 m 个值;
  • 空间复杂度:O(n+m)
  • 稳定性:稳定; 

各种排序方法的综合比较

时间性能

  • 按平均的时间性能来分,有三类排序方法
    • 时间复杂度为 O(n\log n) 的方法有:
      • 快速排序、堆排序和归并排序,其中以快速排序为最好;
    • 时间复杂度为O(n^2) 的有:
      • 直接插入排序、冒泡排序和简单选择排序,其中以直接插入排序为最好;
      • 特别是对那些关键字近似有序的记录序列尤为如此;
    • 时间复杂度为O(n) 的排序方法只有:基数排序;
  • 当待排序记录按关键字顺序有序时,直接插入排序和冒泡排序能达到 O(n) 的时间复杂度; 而对于快速排序而言,这是最不好的情况,此时的时间性能退化为 O(n^2),因此是应该尽量避免的情况。
  • 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变;

空间性能

  • 所以的简单排序方法(包括:直接插入排序、冒泡和简单选择排序)和堆排序的空间复杂度为O(1)
  • 快速排序为 O(\log n),为栈所需要的辅助空间;
  • 归并排序所需要辅助空间最多,其空间复杂度为 O(n)
  • 链式基数排序需附设队列首尾指针,则空间复杂度为O(rd);

排序方法的稳定性

  • 稳定的怕排序方法指的是,对于两个关键字相等的记录,他们在序列中的相对位置,在排序前后不会发生改变;
  • 当对多关键字的记录序列进行 LSD 方法排序时,必须采用稳定的排序方法。
  • 对于不稳定的排序方法,只要能举出一个实例说明即可。
  • 快速排序和堆排序是不稳定的排序方法。(简单选择排序也是不稳定的)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值