当前位置: 首页 > article >正文

从插入排序到希尔排序

从插入排序到希尔排序

插入排序

原理

插入排序是一种简单直观的排序算法,其基本思想是通过将每个元素逐个插入到已排序的部分中,逐步构建一个有序序列。

操作步骤

  1. 初始化:将第 1 个元素视为已经有序的部分(初始时长度为 1)。

  2. 遍历数组:从第 2 个元素开始,依次将其视为待插入的“新”元素。

  3. 查找位置:在已排序部分中找到该元素的正确位置,并将其插入到适当的位置。

  • 从已排序部分的末尾向前查找,找到第 1 个小(大)于等于 arr[i] 的元素位置。

  • 将 arr[i] 插入到该位置之后。

重复步骤:直到所有元素都处理完毕。

排序过程

c683ed33be08ca61062a81a9105bc4f5.png
  • 默认[ 3 ]视为有序部分。

  • 第 1 轮遍历排序 8,将其插入 3 前面,并将 [ 8 3 ] 视为有序部分。

  • 第 2 轮遍历排序 5,将其插入 8 后面,并将 [ 8 5 3 ] 视为有序部分。

  • 第 3 轮遍历排序 1,将其插入 3 后面,并将 [ 8 5 3 1 ] 视为有序部分。

  • 第 4 轮遍历排序 2,将其插入 3 后面,并将 [ 8 5 3 2 1] 视为有序部分。

  • 第 5 轮遍历排序 6,将其插入 8 后面,并将 [ 8 6 5 3 2 1 ] 视为有序部分。

  • 第 6 轮遍历排序 9,将其插入 8 前面,并将 [ 9 8 6 5 3 2 1 ] 视为有序部分。

  • 第 7 轮遍历排序 7,将其插入 8 后面,并将 [ 9 8 7 6 5 3 2 1 ] 视为有序部分。

代码实现

/**
 * @ 简单插入排序
 * @ arr - 待排序的数组     num - 数组元素个数
 * @ 要求从大到小排序数据
 * */ 
void simple_insertion_sort(int *arr, int num)
{
    int i, j, tmp;

    for (i=1; i<num; i++) {
        j = i - 1;      /* 有序部分最后一个索引 */
        tmp = arr[i];   /* 当前待插入的元素 */

        /* 1. 若插入数据比有序部分最后一个元素还小,无需处理 */
        if (arr[j] >= tmp) {
            continue;
        }

        /* 2. 查找带插入数据位置,并后移数据腾出位置 */
        while (j >= 0) {          
            if (arr[j] >= tmp)
                break;

            arr[j+1] = arr[j];
            j--;
        }

        /* 3. 插入到正确位置 */
        arr[j+1] = tmp;
    }
}

技术点

  • 提前终止内层循环:在内层循环中,如果当前已排序部分的第一个元素已经小于等于待插入元素,则无需继续比较,直接结束内层循环。这可以减少不必要的比较操作。

进阶代码

插入排序,可以理解为从序列中第 1 个元素开始,间隔固定 1 个元素,进行的序列排序。

假设程序猿想要在序列中从任意元素 (start_pos) 位置开始,间隔固定元素个数 (gap) 提取成新的序列进行插入排序,此时代码又该如何进行通用设计呢?

/**
 * @ 插入排序
 * @ arr - 待排序的数组                 num - 数组元素个数
 * @ start_pos - 待处理元素起始位置     gap - 间隔
 * @ 要求从大到小排序数据
 * */ 
void insertion_sort(int *arr, int num, int start_pos, int gap)
{
    int i;
    int index;          /* 用于有序部分数据遍历索引 */
    int insert_data;    /* 用于记录待插入的数据 */

    for (i=start_pos+gap; i<num; i+=gap) {
        index = i - gap;        /* 有序部分最后一个索引,从最后一个开始索引 */
        insert_data = arr[i];   /* 当前待插入的元素 */

        /* 1. 若插入数据比有序部分最后一个元素还小,无需处理 */
        if (arr[index] >= insert_data) {
            continue;
        }

        /* 2. 查找带插入数据位置,并后移数据腾出位置 */
        while (index >= 0) {          
            if (arr[index] >= insert_data)
                break;

            arr[index + gap] = arr[index];
            index -= gap;
        }

        /* 3. 插入到正确位置 */
        arr[index + gap] = insert_data;
    }
}
  • 注意:我们只需将start_pos = 0 且 gap = 1 就相当于简单插入排序。

运行结果


ece47449e3c1b9c27eb84ca995004761.png

总结

插入排序作为基础排序算法之一,虽然在最坏情况下性能不佳,但在某些特定场景下仍然具有其独特的优势。通过优化如“提前终止内层循环”等方法,可以在一定程度上提升效率,使其更适合于小规模数据或部分有序的数据。然而,对于大规模数据来说,仍然建议使用更高效的排序算法。

希尔排序

原理

希尔排序是一种改进的插入排序,它允许在远距离的元素之间进行比较和交换,从而减少排序的时间复杂度。

  1. 确定步长:选择一个初始步长 gap,通常取数组长度的一半。

  2. 分组排序:将数组分成若干个子序列,每个子序列中的元素间隔为当前的步长 gap

  3. 逐步缩小步长:对每一个子序列进行插入排序或其他排序算法(如交换排序),然后减小步长 gap,重复分组和排序的过程,直到步长为1,此时整个数组视为一个子序列,进行一次插入排序。

操作步骤

  1. 初始化步长:设置初始步长 gap = n / 2,其中 n 是数组的长度。

  2. 循环调整步长:当前步长减半:gap = gap / 2 直到步长为 0

  3. 分组排序:对于每个子序列(元素间隔为当前步长),进行插入排序或其他稳定排序算法。

排序过程

1e4747376233c2fb6ff384cf5ebbd4b1.png

  • 确定序列元素数量 n=8

  • 第 1 次排序

    • 将序列分为  n/2 = 4 组,每组固定间隔  gap = n/2 = 4 元素。

    • 分组情况 [ 3 2 ]   [ 8 6 ]   [ 5 9 ]   [ 1 7 ]。

    • 对于每组进行插入排序得到 [ 3 2 ]   [ 8 6 ]   [ 9 5]   [ 7 1]。

    • 合并之后的序列为 [ 3 8 9 7 2 6 5 1]。

  • 第 2 次排序

    • 将序列分为  gap/2 = 2组,每组固定间隔  gap = gap/2 = 2元素。

    • 分组情况 [ 3 9 2 5 ]   [ 8 7 6 1 ]。

    • 对于每组进行插入排序得到 [ 9 5 3 2 ]   [ 8 7 6 1 ]。

    • 合并之后的序列为 [ 9 8 5 7 3 6 2 1]。

  • 第 3 次排序

    • 将序列分为  gap/2 = 1组,每组固定间隔  gap = gap/2 = 1元素。

    • 等同于简单插入排序。

    • 排序之后的序列为 [ 9 8 7 6 5 3 2 1]。

代码实现

/**
 * @ 希尔排序,插入排序的优化
 * @ arr - 待排序的数组     num - 数组元素个数
 * @ 要求从大到小排序数据
 * */ 
void shell_sort(int *arr, int num)
{
    int i;
    int gap = num / 2;    /* 控制间距, 默认从 num/2 开始 */

    while (gap > 0) {           /* 控制间距 */
        for (i=0; i<gap; i++) { /* 控制组数 */
            /* 调用插入排序 */
            insertion_sort(arr, num, i, gap);
        }
        gap /= 2;
    }
}

分析

  • 希尔排序主要还是引入了分组的概念,通过分组将序列拆分成小序列,并且根据间隔实现远距离数据的快速排序,从而减少数据比较与移位的处理,降低时间复杂度。

  • 相对于插入排序,希尔排序主要多了两层循环,一层用于控制间隔 gap,一层用于当前间隔下各个组的插入排序处理。

运行结果

a80b7335e3a4962292b90c3a36ac4dbd.png

缺点

  • 希尔排序在大多数实现中并不是稳定的排序算法。这意味着,当两个相等的元素出现时,它们的相对顺序可能会被打乱。

  • 在某些特定的数据分布下,希尔排序的时间复杂度可能退化到O(n²),这与普通的插入排序相当,无法发挥其高效的理论优势。

  • 希尔排序的性能严重依赖于步长序列的选择。如果步长选择不当,可能导致算法效率低下或排序失败。

优化方法

  • 改进步长序列的选择:使用更科学的步长序列,如Sedgewick提出的步长序列:1, 3, 7, 15, 31,…。这种步长序列可以在一定程度上减少排序过程中的逆序数,提高算法效率。

  • 确保稳定性:在比较和交换元素时,除了比较大小外,还应考虑元素的原始位置。例如,在比较两个相等的元素时,优先保留原位置较前的元素。这可以通过在键值对中包含索引信息来实现。

  • 优化排序过程中的细节:在内循环中,尽量减少不必要的数据移动和比较操作。例如,当发现当前元素已经位于正确的位置时,可以提前终止内层循环。

  • 结合其他优化技术:将希尔排序与其他排序算法的优点结合起来,如在小规模的数据子集上使用更高效的排序方法(如归并排序或快速排序)进行优化。

完整代码

/**
 * @Filename : insertion_sort.c
 * @Revision : $Revision: 1.00 $
 * @Author : Feng(更多编程相关的知识和源码见微信公众号:不只会拍照的程序猿,欢迎订阅)
 * @Description : 从插入排序一步一步到希尔排序
**/

#include <stdio.h>
#include <string.h>

#define MAX_NUM     8  
staticint cnt = 0; /* 统计排序次数 */

/**
 * @ 打印数组
 * @ arr - 待打印的数组     num - 数组元素个数
 * */
void print_arr(int *arr, int num) 
{
    int i;

    for (i=0; i<num; i++)
        printf("%02d ", arr[i]);
    printf("\n");
}

/**
 * @ 插入排序
 * @ arr - 待排序的数组                 num - 数组元素个数
 * @ start_pos - 待处理元素起始位置     gap - 间隔
 * @ 要求从大到小排序数据
 * */
void insertion_sort(int *arr, int num, int start_pos, int gap)
{
    int i;
    int index;          /* 用于有序部分数据遍历索引 */
    int insert_data;    /* 用于记录待插入的数据 */

    for (i=start_pos+gap; i<num; i+=gap) {
        index = i - gap;        /* 有序部分最后一个索引,从最后一个开始索引 */
        insert_data = arr[i];   /* 当前待插入的元素 */

        cnt++;

        /* 1. 若插入数据比有序部分最后一个元素还小,无需处理 */
        if (arr[index] >= insert_data) {
            printf("无需移位 第%02d次排序: ", cnt);
            print_arr(arr, num);
            continue;
        }

        /* 2. 查找带插入数据位置,并后移数据腾出位置 */
        while (index >= 0) {          
            if (arr[index] >= insert_data)
                break;

            arr[index + gap] = arr[index];
            index -= gap;
        }

        /* 3. 插入到正确位置 */
        arr[index + gap] = insert_data;

        printf("需要移位 第%02d次排序: ", cnt);
        print_arr(arr, num);
    }
}

/**
 * @ 简单插入排序
 * @ arr - 待排序的数组     num - 数组元素个数
 * @ 要求从大到小排序数据
 * */
void simple_insertion_sort(int *arr, int num)
{
    printf("排序前数组内容: ");
    print_arr(arr, num);

    cnt = 0;    /* 清空统计次数 */
    insertion_sort(arr, num, 0, 1);

    printf("排序后数组内容: ");
    print_arr(arr, num);
}

/**
 * @ 希尔排序,插入排序的优化
 * @ arr - 待排序的数组     num - 数组元素个数
 * @ 要求从大到小排序数据
 * */
void shell_sort(int *arr, int num)
{
    int i;
    int gap = num / 2;    /* 控制间距, 默认从 num/2 开始 */

    printf("排序前数组内容: ");
    print_arr(arr, num);

    cnt = 0;    /* 清空统计次数 */

    while (gap > 0) {           /* 控制间距 */
        for (i=0; i<gap; i++) { /* 控制组数 */
            /* 调用插入排序 */
            insertion_sort(arr, num, i, gap);
        }
        gap /= 2;
    }

    printf("排序后数组内容: ");
    print_arr(arr, num);
}

int main(void)
{
    int buf1[] = {3, 8, 5, 1, 2, 6, 9, 7};
    int buf2[] = {3, 8, 5, 1, 2, 6, 9, 7};

    printf ("----------简单插入排序---------\n");
    simple_insertion_sort(buf1, MAX_NUM);

    printf ("---------希尔排序---------\n");
    shell_sort(buf2, MAX_NUM);
    
    return0;
}

运行结果

c57af05138c3eb103dd0e2b23c4efe99.png

http://www.kler.cn/a/552588.html

相关文章:

  • A与B组件自动对齐与组装,无映射直接补偿。
  • SpingBoot-Vue 前后端分离—实现钉钉免登功能(2025)
  • 【Spring+MyBatis】_图书管理系统(中篇)
  • c# -01新属性-模式匹配、弃元、析构元组和其他类型
  • spark大数据分析
  • python-leetcode-最小路径和
  • vue中使用富文本编辑器
  • SpringBoot速成(14)文件上传P23-P26
  • GIT:如何合并已commit的信息并进行push操作
  • 【达梦数据库】dblink连接[SqlServer/Mysql]报错处理
  • Edge浏览器清理主页
  • Copilot Next Edit Suggestions(预览版)
  • nodejs:express + js-mdict 网页查询英汉词典,能显示图片
  • 【Java 面试 八股文】并发编程篇
  • The First项目报告:重塑链上游戏生态,解读B3 Base的双赢局面
  • Pytorch实现之粒子群优化算法在GAN中的应用
  • 一周学会Flask3 Python Web开发-post请求与参数获取
  • TCP通讯-客户端链接
  • mysql 学习16 视图,存储过程,存储函数,触发器
  • MySQL中的事务隔离级别有哪些?