插入排序(直接、折半、二路、希尔)

【往期】六大查找算法

【三大经典查找】跳跃、插值、斐波那契

【三大基础查找】顺序、二分、分块 

目录

【往期】六大查找算法

前言

一、稳定的排序?

二、直接插入排序O(n^2)

1.使用场景

2.执行步骤

三、折半插入排序O(n^2)

1.执行步骤 

四、2-路插入排序 最坏O(n^2)

五、希尔排序

1.使用场景

2.执行步骤 


前言

  查找和排序是数据结构与算法中不可或缺的一环,是前辈们在算法道路上留下的重要且方便的一些技巧,学习这些经典的查找和排序也能让我们更好和更快的解决问题。在这个专栏中我们会学习六大查找和十大排序,而本篇将详细讲解其中的插入排序——直接插入排序、折半(二分)插入排序、2-路插入排序;

  注意:本文中所有排序按照升序排序,降序只需要把逻辑反过来即可!


一、稳定的排序?

在学习排序算法之前我们先要了解一些关于排序的概念:

  • 就地排序:只需要使用恒定的额外空间和给定的数组,空间复杂度为O(1);
  • 外部排序和内部排序:看待排序数据是否能一次载入到内存之中,如果不能就只能使用外排序;
  • 什么是稳定排序?所谓“稳定”:判断相同的关键字排序前后相对位置的变化,如数列3 2 2 5 排序后为 2 2 3 5 ,如果其中2和2相对位置没有改变,那么这个排序算法就是稳定的;
  • 不稳定的排序就意味着交换的次数多,如果需要按多条件排序就会乱;排序算法的稳定性在处理多数据、键值对时很重要;

排序的概念很简单,重点了解稳定性的概念其他的过一遍即可;


二、直接插入排序O(n^2)

1.使用场景

如果有了解过C++STL底层的同学应该知道,在以前sort排序的底层用的都是快速排序,不过快排其实是一种不稳定的排序,现在sort中是用多种排序对快排进行了补充,而这个直接插入排序就是当数据量小于8时,对快排的一个补充;

2.执行步骤

  • 基本思路:将待排序数组分成两个序列,前面的序列保持有序,依次选择后面的元素往前插入,是就地排序;

如下所示一个数组,第一步:把arr[0]看做有序,比较arr[0] < arr[1],目前认为arr[1]有序;

第二步:比较arr[1] > arr[2],交换;比较arr[0] < arr[1],认为位置正确停止交换,继续;

第三步:比较arr[2] > arr[3],交换;比较arr[1] > arr[2],交换;比较arr[0] < arr[1]认为位置正确停止交换,重复上述步骤;

直到全部数都认为有序了,排序结束;

 代码实现也非常简单

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

//直接插入排序
void insertSort(int a[], int n)
{
    if(a == NULL)
    {
        return;
    }
        
    //将a[i]插入到a[i -1]、a[i - 2]...
    for(int i = 1; i < n; i++)
    {
        for(int j = i; j > 0 && a[j] < a[j - 1]; j--)
        {
            //后一个小于前一个 交换
            int temp = a[j];
            a[j] = a[j - 1];
            a[j - 1] = temp;
        }
    }
}

int main()
{
    int arr[] = {9, 10, 13, 20, 12};
    int length = sizeof(arr) / sizeof(arr[0]);
    insertSort(arr, length);
    for(int i = 0; i < length; i++)
    {
        printf("%d \n", arr[i]);
    }
    system("pause");
    return 0;
}

 运行结果

 


三、折半插入排序O(n^2)

折半插入排序思想与折半(二分)查找是一样的,就是折半(二分)查找的应用;

1.执行步骤 

  • 顺序的把待查找的序列中各个元素按照其关键字的大小 通过折半(二分)查找插入到序列当中;

有一个数组 { 0, 9, 10, 12, 20, 8, 5, 7, 1};

以数字8为例,我们想要找到它在数组中正确的位置,(假设元素8之前的序列已经按照规则排好序了)我们只需要按照二分查找的规则,不断的缩小待查找区间,直到左值域low大于右值域high,这时我们找到的指针mid就是待排元素所在数组中正确的位置;

找到了正确的位置之后就是插值,因为在数组中不能直接插入,需要把待区间【mid,i】(i为待排数字的下标)之间的元素全部从后往前移动;

折半插入排序的逻辑和代码都十分简单,关键是学会二分查找的思想和在数组中元素怎样移动;


四、2-路插入排序 最坏O(n^2)

2-路插入排序是在折半排序的基础上作出了改进,其目的是减少排序过程中数据的移动次数:

  • 另外设置一个和待排序数组相同大小的环形数组D,将无序表中第一个记录添加进D[0],从无序表的第二个位置开始和D[0]比较如果大于,就添加到右边,反之左边;

关于环形数组的知识可以看@康来个程的数据结构中的环形队列和环形数组思想是一样的;

源码如下:代码的难点就是在于怎么理解模运算,多在纸上画画就很清楚了;

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

//二路插入排序
void twoWayInsert(int a[], int n, int temp[])
{
    int first, final;//分别记录temp数组的最大值和最小值
    first = final = 0;
    temp[0] = a[0];
    for(int i = 1; i < n; i++)
    {
        //插入的元素比最小的元素都要小
        if(a[i] < temp[first])
        {
            first = (first - 1 + n) % n;
            temp[first] = a[i];
        }
        //待插入的元素要比最大值还大
        else if(a[i] > temp[final])
        {
            final = (final + 1 + n) % n;
            temp[final] = a[i];
        }
        else
        {
            int k = (final + 1 + n) % n;
            //当插入的值比当前值小的时候 需要移动当前值的位置
            while(temp[((k - 1) + n) % n] > a[i])
            {
                temp[(k + n) % n] = temp[(k - 1 + n) % n];
                k = (k - 1 + n) % n;
            }
            temp[(k + n) % n] = a[i];
            //插入元素 因为最大值的位置发生了改变 所以更新最大值的下标
            final = (final + 1 + n) % n;
        }
    }
    //将元素放回原数组中
    for(int i = 0; i < n; i++)
    {
        a[i] = temp[(first + i) % n];
    }
}

int main()
{
    int arr[] = {9, 10, 13, 20, 12};
    int length = sizeof(arr) / sizeof(arr[0]);
    int* temp = (int*)malloc(sizeof(temp) * length);
    twoWayInsert(arr, length, temp);
    for (int i = 0; i < length; i++)
    {
        printf("%d ", arr[i]);
    }
    
    system("pause");
    return 0;
}

运行结果 


五、希尔排序

1.使用场景

  • 希尔排序也叫做缩小增量排序,它采用了跳跃的策略,通过某一增量gap,将数组划分成若干组;
  • 把待排序数组按照下标记录的一些增量排序分组,对于每组进行直接插入排序,当随着增量的减小 每组包含的关键字越来越多,当增量减少到1时就变成一组了;

2.执行步骤 

有一个数组{8, 9, 1, 7, 2, 3, 5, 4, 6, 0}

假定)增量gap = 数组长度/2 = 5(增量的选取是一个复杂的数学问题,选取的增量不同,复杂度也不同)再对这几组进行直接插入排序,相当于第一次分组值进行一次交换;

 gap = 5 / 2 = 2;

gap = 2 / 2 = 1;

 希尔排序的代码实现也很简单,排序的关键在于增量gap的选取

像是对数组{ 1,9, 2,10,3,11, 4,12,5,13,6,14,7,15,8,16 }还是选取增量初始化为gap = n / 2 = 8的话,就会发现前三次排序都没有变化,也就是这个数组最坏的情况复杂度为O(n^2),要注意增量元素不能互为质数!

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

//希尔排序
void shellSort(int a[], int n)
{
    int gap;//步长
    int k;                               
    for (gap = n / 2; gap > 0; gap/=2) //步长的选取
    {
        //直接插入排序
        for (int i = 0; i < gap; i++)
        {
            //加上每一趟的步长 交换排序
            for (int j = i + gap; j < n; j+=gap)
            {
                //如果当前元素小于前驱元素 就交换
                if (a[j] < a[j - gap])
                {
                    int temp = a[j];
                    //将k初始化为j的前一个元素(与j相差gap个长度)
                    k = j - gap;
                    //记录后移查找插入的位置
                    while (k >= 0 && a[k] > temp)
                    {
                        a[k + gap] = a[k];//将a[j]前面的且大于temp的值的元素向后移动
                        k-=gap;
                    }
                    a[k + gap] = temp;
                }
            }
        }
    }
}

int main()
{
    int arr[] = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
    int length = sizeof(arr) / sizeof(arr[0]);
    shellSort(arr, length);
    for(int i = 0; i < length; i++)
    {
        printf("%d", arr[i]);
    }
    system("pause");
    return 0;
}

运行结果 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小白还在写代码

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

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

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

打赏作者

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

抵扣说明:

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

余额充值