从插入排序到希尔排序

从插入排序到希尔排序

插入排序

原理

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

操作步骤

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

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

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

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

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

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

排序过程

  • 默认[ 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 ] 视为有序部分。

代码实现

go 复制代码
/**
 * @ 简单插入排序
 * @ 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) 提取成新的序列进行插入排序,此时代码又该如何进行通用设计呢?

go 复制代码
/**
 * @ 插入排序
 * @ 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 = 0gap = 1 就相当于简单插入排序。

运行结果

总结

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

希尔排序

原理

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

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

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

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

操作步骤

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

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

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

排序过程

  • 确定序列元素数量 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]。

代码实现

go 复制代码
/**
 * @ 希尔排序,插入排序的优化
 * @ 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,一层用于当前间隔下各个组的插入排序处理。

运行结果

缺点

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

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

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

优化方法

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

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

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

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

完整代码

go 复制代码
/**
 * @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;
}

运行结果

相关推荐
查理零世5 分钟前
【蓝桥杯集训·每日一题2025】 AcWing 6134. 哞叫时间II python
python·算法·蓝桥杯
仟濹6 分钟前
【二分搜索 C/C++】洛谷 P1873 EKO / 砍树
c语言·c++·算法
10km12 分钟前
java:Apache Commons Configuration2占位符解析异常的正确解法:${prefix:name:-default}
java·apache·configuration2·变量插值·interpolation
customer0812 分钟前
【开源免费】基于SpringBoot+Vue.JS个人博客系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
紫雾凌寒15 分钟前
解锁机器学习核心算法|神经网络:AI 领域的 “超级引擎”
人工智能·python·神经网络·算法·机器学习·卷积神经网络
灰色人生qwer20 分钟前
SpringBoot 项目配置日志输出
java·spring boot·后端
2301_7930698230 分钟前
Spring Boot +SQL项目优化策略,GraphQL和SQL 区别,Spring JDBC 等原理辨析(万字长文+代码)
java·数据库·spring boot·sql·jdbc·orm
阿华的代码王国36 分钟前
【从0做项目】Java搜索引擎(6)& 正则表达式鲨疯了&优化正文解析
java·后端·搜索引擎·正则表达式·java项目·从0到1做项目
服务端相声演员36 分钟前
Oracle JDK、Open JDK zulu下载地址
java·开发语言
是姜姜啊!37 分钟前
java连接redis
java·redis