排序---希尔排序(Shell Sort)

发布于:2025-09-11 ⋅ 阅读:(21) ⋅ 点赞:(0)
一、希尔排序的起源与定义

希尔排序是1959年由美国计算机科学家唐纳德·希尔(Donald Shell)提出的排序算法,其本质是对插入排序的优化与改进,因此也被称为“缩小增量排序”(Diminishing Increment Sort)。

插入排序在处理近乎有序的数组时效率很高(时间复杂度接近O(n)),但在面对无序数组时,每次元素可能需要移动大量位置(最坏情况O(n²))。希尔排序通过引入“增量”概念,将数组分割为多个子序列并分别排序,逐步缩小增量直至为1,最终通过一次完整的插入排序完成整个数组的排序,从而大幅提升效率。

二、核心思想

希尔排序的核心思想可概括为“分组插入排序+逐步缩小增量”:

  1. 分组:选择一个增量(gap),将数组按下标间隔为gap的规则分为若干子序列(例如,gap=3时,下标0、3、6…为一组,1、4、7…为另一组)。
  2. 子序列排序:对每个子序列执行插入排序,使每组内的元素相对有序。
  3. 缩小增量:减小gap的值(如gap=gap/2),重复分组和排序步骤。
  4. 最终排序:当gap=1时,整个数组被视为一个子序列,执行最后一次插入排序,此时数组已接近有序,插入排序效率极高。

通过这一过程,元素可以“跳跃式”地向最终位置移动,减少了插入排序中频繁的相邻元素交换,从而优化性能。

三、增量序列的选择

增量序列(gap序列)的选择是希尔排序性能的关键,直接影响算法效率。常见的增量序列有:

  1. 希尔原始增量gap = n/2, n/4, ..., 1(n为数组长度)。
    优点是简单易实现;缺点是最坏时间复杂度仍为O(n²),且增量间可能存在公因子(如4和2),导致分组重复。

  2. Knuth增量gap = (3^k - 1)/2(从小于n的最大增量开始,逐步减小)。
    例如,n=100时,增量序列为40、13、4、1。该序列避免了公因子问题,最坏时间复杂度优化至O(n^(3/2))。

  3. Hibbard增量gap = 2^k - 1(如1、3、7、15…)。
    最坏时间复杂度为O(n^(3/2)),实践中性能优于希尔原始增量。

  4. Sedgewick增量gap = 9*4^k - 9*2^k + 14^k - 3*2^k + 1(如1、5、19、41…)。
    目前已知性能较优的增量序列,最坏时间复杂度接近O(n^1.3)。

实际应用中,Knuth增量因实现简单且性能稳定,是常用选择。

四、工作原理示例

以数组 [12, 34, 54, 2, 3, 9, 8, 76, 52, 1](n=10)为例,使用Knuth增量序列(初始gap=4,因(3³-1)/2=13>10,故取(3²-1)/2=4),演示希尔排序过程:

  1. 第一轮:gap=4
    按间隔4分组,得到4个子序列:

    • 组1:[12, 9, 1]
    • 组2:[34, 8]
    • 组3:[54, 76]
    • 组4:[2, 52]

    对每组执行插入排序后,子序列变为:

    • 组1:[1, 9, 12]
    • 组2:[8, 34]
    • 组3:[54, 76]
    • 组4:[2, 52]

    合并后数组:[1, 8, 54, 2, 3, 9, 76, 52, 12, 34]

  2. 第二轮:gap=1(Knuth增量下一个为(3¹-1)/2=1)
    此时gap=1,整个数组为一个子序列:[1, 8, 54, 2, 3, 9, 76, 52, 12, 34]
    执行插入排序,最终得到有序数组:[1, 2, 3, 8, 9, 12, 34, 52, 54, 76]

五、时间复杂度与空间复杂度
  • 时间复杂度
    希尔排序的时间复杂度依赖于增量序列,目前尚未有精确的数学证明。在常见增量序列中:

    • 希尔原始增量:最坏O(n²),平均O(n^1.5);
    • Knuth增量:最坏O(n^(3/2));
    • Sedgewick增量:最坏接近O(n^1.3)。
  • 空间复杂度
    仅需一个临时变量用于元素交换,因此空间复杂度为O(1)(原地排序)。

六、优缺点分析
  • 优点

    1. 效率高于简单插入排序,尤其对中等规模数组(n=1000~10000)表现优异;
    2. 原地排序,空间复杂度低;
    3. 实现简单,易于理解和编码。
  • 缺点

    1. 增量序列选择影响性能,需根据场景调整;
    2. 不稳定排序(相同元素可能因分组排序改变相对位置);
    3. 对大规模数据(n>10^5)效率不如快速排序、归并排序等高级算法。
七、C++实现示例

以下代码使用Knuth增量序列实现希尔排序,并包含测试用例:

#include <iostream>
#include <vector>
using namespace std;

// 希尔排序函数(使用希尔原始增量序列)
void shellSort(vector<int>& arr) {
    int n = arr.size();
    
    // 希尔原始增量:初始为n/2,每次减半直至为1
    for (int gap = n / 2; gap > 0; gap /= 2) {
        // 对每个子序列执行插入排序
        for (int i = gap; i < n; i++) {
            int temp = arr[i];  // 待插入元素
            int j;
            
            // 子序列内的插入排序(与同组前序元素比较)
            for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                arr[j] = arr[j - gap];  // 元素后移
            }
            arr[j] = temp;  // 插入待排序元素
        }
    }
}

// 打印数组
void printArray(const vector<int>& arr) {
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
}

// 测试
int main() {
    vector<int> arr = {12, 34, 54, 2, 3, 9, 8, 76, 52, 1};
    cout << "排序前:";
    printArray(arr);

    shellSort(arr);

    cout << "排序后:";
    printArray(arr);
    return 0;
}
    
八、代码解析
  1. 增量计算
    先通过 gap = gap * 3 + 1 计算小于数组长度的最大Knuth增量(如n=10时,gap初始为4)。

  2. 分组排序
    外层循环控制增量 gap 从大到小变化(gap = (gap-1)/3);中层循环遍历数组元素,从 gap 开始(跳过前gap个元素,它们是各子序列的第一个元素);内层循环对每个子序列执行插入排序——将 arr[i] 与同组前序元素(arr[j-gap])比较,若前序元素更大则后移,最终将 arr[i] 插入正确位置。

  3. 测试结果
    输入数组 [12, 34, 54, 2, 3, 9, 8, 76, 52, 1] 经排序后输出 [1, 2, 3, 8, 9, 12, 34, 52, 54, 76],符合预期。

九、应用场景

希尔排序适用于以下场景:

  • 中等规模数据(n=1000~10000)的排序;
  • 对空间复杂度要求严格(需原地排序)的场景;
  • 嵌入式系统或资源受限设备(算法简单,内存占用低)。

对于大规模数据,建议使用快速排序、归并排序等更高效的算法;若需稳定排序,可选择归并排序或基数排序。


希尔排序通过分组插入排序和逐步缩小增量的策略,有效优化了插入排序的性能,是一种兼顾简单性与效率的排序算法。其核心在于增量序列的选择,合理的增量可显著提升排序速度。尽管在极端场景下性能不及高级排序算法,但希尔排序因实现简单、空间效率高,仍是实践中常用的排序方案之一。


网站公告

今日签到

点亮在社区的每一天
去签到