排序算法技术文档

目录


一、插入排序(Insertion Sort)

1. 基本思想

  • 将数组划分为两个逻辑区域:
    • 已排序区
    • 未排序区
  • 默认第一个元素在已排序区
  • 每次从未排序区取出一个元素
  • 从已排序区从后向前比较
  • 找到合适位置插入

2. 核心特征

  • 类似“打扑克牌插牌”
  • 数据基本有序时效率极高
  • 属于稳定排序
  • 原地排序(不需要额外数组)

3. 时间 & 空间复杂度

情况 复杂度
最好情况(已排序) O(n)
平均情况 O(n²)
最坏情况(逆序) O(n²)
空间复杂度 O(1)
稳定性 稳定

4. 适用场景

  • 数据量较小
  • 数据基本有序
  • 作为其他排序(如希尔排序)的基础

5. 代码实现

class 插入排序
{
    //时间复杂度:O(n²)
    //空间复杂度:O(1) 所有操作都是在原数组里完成的,没有开辟新的数组或额外存储。
    //潜在隐患:在数据量较大时,算法会非常慢,可能导致程序卡顿甚至无法在合理时间内完成。如果在安全关键场景(如实时系统、金融交易系统)使用,会造成延迟风险
    //应用场景:数据量较小 数据基本有序
    class Program
    {
        static void Main(string[] args)
        {
            #region 知识点一 插入排序的基本原理
            //871542639
            //两个区域
            // 排序区
            // 未排序区
            // 用一个索引值做分水岭

            // 未排序区元素
            // 与排序区元素比较
            //插入到合适位置
            // 直到未排序区清空
            #endregion

            #region 知识点二 代码实现
            //实现升序 把 大的 放在最后面
            int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
            //int[] arr = new int[] { 12, 5, 33, 2, 6, 9, 5, 4, 6, 6, 7, 6 };
            //前提规则
            //排序开始前
            //首先认为第一个元素在排序区中
            //其它所有元素在未排序区中

            //排序开始后
            //每次将未排序区第一个元素取出用于和
            //排序区中元素比较(从后往前)
            //满足条件(较大或者较小)
            //则排序区中元素往后移动一个位置。

            //注意
            //所有数字都在一个数组中
            //所谓的两个区域是一个分水岭索引

            //第一步
            //能取出未排序区的所有元素进行比较
            //i=1的原因:默认第一个元素就在排序区
            for (int i = 1; i < arr.Length; i++)
            {
                //第二步
                //每一轮
                //1.取出排序区的最后一个元素索引
                int sortIndex = i - 1;
                //2.取出未排序区的第一个元素
                int noSortNum = arr[i];

                //第三步
                //在未排序区进行比较
                //移动位置
                //确定插入索引
                while (sortIndex >= 0 && arr[sortIndex] > noSortNum)
                {
                    //只要进了这个while循环 证明满足条件
                    //排序区中的元素 就应该往后退一格
                    arr[sortIndex + 1] = arr[sortIndex];
                    //移动到排序区的前一个位置 准备继续比较
                    sortIndex--;
                }
                //最终插入数字
                //循环中只是在确定位置 和找最终的插入位置
                //最终插入对应位置 应该循环结束后
                arr[sortIndex + 1] = noSortNum;
            }

            for (int i = 0; i < arr.Length; i++)
            {
                Console.WriteLine(arr[i]);
            }
            #endregion

            #region 知识点三 总结
            //为什么有两层循环
            //第一层循环:一次取出未排序区的元素进行排序
            //第二层循环:找到想要插入的位置

            //为什么第一层循环从1开始遍历
            //插入排序的关键是分两个区域
            //已派序区和未排序区
            //默认第一个元素在已派序区

            //为什么使用while循环
            //满足条件才比较
            //否则证明插入位置已确定
            //不需要继续循环

            //为什么可以直接往后移位置
            //每轮未排序数已记录
            //最后一个位置不怕丢

            //为什么确定位置后 是放在sortIndex + 1的位置
            //当循环停止时 插入位置应该是停止循环的索引加1处

            //基本原理
            //两个区域
            //用索引值来区分
            //未排序区与排序区
            //元素不停比较
            //找到合适位置
            //插入当前元素

            //套路写法
            //两层循环
            //一层获取未排序区元素
            //一层找到合适插入位置

            //注意事项
            //默认开头已排序
            //第二层循环外插入
            #endregion
        }
    }
}

二、希尔排序(Shell Sort)

1. 基本思想

希尔排序是 插入排序的升级版

  • 引入 步长(gap)
  • 将原数组按步长拆分为多个子序列
  • 对每个子序列执行插入排序
  • 步长逐渐缩小,最终为 1

2. 排序流程

  1. 初始步长:gap = n / 2
  2. 对 gap 个子序列分别做插入排序
  3. gap /= 2
  4. 重复直到 gap = 1

3. 时间 & 空间复杂度

时间复杂度依赖步长序列

项目 说明
最坏情况 O(n²)
平均情况 介于 O(n) ~ O(n²)
实际表现 明显优于插入排序
空间复杂度 O(1)
稳定性 不稳定

4. 适用场景

  • 中等规模数据
  • 内存受限环境
  • 插入排序性能不足时的改进方案

5. 代码实现

namespace 希尔排序
{
    //时间复杂度:取决于步长序列 O(n²) 在实际应用中,通常接近 O(n log² n),比插入排序快很多 
    //空间复杂度:O(1)  - 原地排序,所有操作都在原数组里完成,没有额外存储 
    //潜在隐患:步长序列选择不当时,性能提升有限,甚至退化为插入排序 实现时要注意索引边界,避免数组越界 
    //应用场景:中等规模数据排序(比插入排序更适合大数据)内存受限环境(空间复杂度 O(1)) 教学和算法研究中常用,作为插入排序的升级版
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("希尔排序");
            #region 知识点一 希尔排序的基本原理
            //希尔排底是
            //插入排序的升级版
            //必须先掌握插入排序

            //希尔排序的原理
            //将整个待排序序列
            //分割成为若干子序列
            //分别进行插入排序

            //总而言之
            //希尔排序对插入排序的升级主要就是加入了一个步长的概念
            //通过步长每次可以把原序列分为多个子序列
            //对子序列进行插入排序
            //在极限情况下可以有效降低普通插入排序的时间复杂度
            //提升算法效率
            #endregion

            #region 知识点二 代码实现
            int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };

            //学习希尔排序的前提条件
            //先掌握插入排序
            //第一步:实现插入排序

            //第二步:确定步长
            //基本规则:每次步长变化都是/2
            //一开始步长 就是数组的长度/2
            //之后每一次 都是在上一次的步长基础上/2
            //结束条件是 步长 <=0
            for (int step = arr.Length / 2; step > 0; step /= 2)
            {
                //第三步:执行插入排序
                //i=1代码 相当于 代表取出来的排序区的第一个元素
                //for(int i=1;i<arr.Length;i++)
                //i=step 相当于 代表取出来的排序区的第一个元素
                for (int i = step; i < arr.Length; i++)
                {
                    //得出未排序区的元素
                    int noSortNum = arr[i];
                    //得出排序区中最后一个元素索引
                    //int sortIndex = i-1;
                    //i-step 代表和子序列中 已排序区元素一一比较
                    int sortIndex = i - step;
                    //进入条件
                    //首先排序区中还有可以比较的 >= 0
                    //排序区中元素 满足交换条件 升序就是排序区中元素要大于未排序区中元素
                    while (sortIndex >= 0 && arr[sortIndex] > noSortNum)
                    {
                        arr[sortIndex + step] = arr[sortIndex];
                        sortIndex -= step;
                    }
                    //找到位置过后 真正的插入 值
                    arr[sortIndex + step] = noSortNum;
                }
            }

            for (int i = 0; i < arr.Length; i++)
            {
                Console.WriteLine(arr[i]);
            }
            #endregion

            #region 知识点三 总结
            //基本原理
            //设置步长
            //步长不停缩小
            //到1排序后结束

            //具体排序方式
            //插入排序原理

            //套路写法
            //三层循环
            //-层获取步长
            //一层获取未排序区元素
            //一层找到合适位置插入

            //注意事项
            //步长确定后
            //会将所有子序列进行插入排序
            #endregion
        }
    }
}

三、归并排序(Merge Sort)

1. 基本思想

分治思想 + 递归

  • 不断将数组二分
  • 直到子数组长度为 1
  • 逐层向上合并
  • 合并过程保证有序

2. 核心流程

  1. 递归拆分数组
  2. 左右子数组分别排序
  3. 合并两个有序数组

3. 时间 & 空间复杂度

项目 复杂度
最好 / 平均 / 最坏 O(n log n)
空间复杂度 O(n)
稳定性 稳定

4. 特点与应用

  • 性能稳定

  • 适合处理大数据量

  • 常用于:

    • 外部排序
    • 链表排序
  • 缺点:额外内存消耗

5. 代码实现

namespace 归并排序
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("归并排序");
            #region 归并排序基本原理
            //归并=递归 +合并

            //数组分左右
            //左右元素相比较
            //满足条件放入新数组
            //-侧用完放对面

            //递归不停分
            //分完再排序
            //排序结束往上走
            //边走边合并
            //走到头顶出结果

            //归并排序分成两部分
            //1.基本排序规则
            //2.递归平分数组

            //递归平分数组:
            //不停进行分割
            // 长度小于停止
            //开始比较
            // 一层一层向上比

            //基本排序规则:
            //左右元素进行比较
            //依次放入新数组中
            //一侧没有了另一侧直接放入新数组
            #endregion

            int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
            arr = Merge(arr);
            for (int i = 0; i < arr.Length; i++)
            {
                Console.WriteLine(arr[i]);
            }
        }
        #region 代码实现
        //第一步:
        //基本排序规则
        //左右元素相比较
        //满足条件放进去
        //一侧用完直接放
        public static int[] Sort(int[] left, int[] right)
        {
            //先准备一个新数组
            int[] array = new int[left.Length + right.Length];
            int leftIndex = 0;//左数组索引
            int rightIndex = 0;//右数组索引

            //最终目的是要填满这个新数组
            //不会出现两侧都放完了 还会进入循环 因为新数组长度是根据左右两个数组计算来的
            for (int i = 0; i < array.Length; i++)
            {
                //左侧放完了 放对面
                if (leftIndex >= left.Length)
                {
                    array[i] = right[rightIndex];
                    //已经放入了右侧元素进入新数组
                    //所以 标识应该指向下一个
                    rightIndex++;
                }
                //右侧放完了 放对面
                else if (rightIndex >= right.Length)
                {
                    array[i] = left[leftIndex];
                    //已经放入了左侧元素进入新数组
                    //所以 标识应该指向下一个
                    leftIndex++;
                }
                else if (left[leftIndex] < right[rightIndex])
                {
                    array[i] = left[leftIndex];
                    //已经放入了左侧元素进入新数组
                    //所以 标识应该指向下一个
                    leftIndex++;
                }
                else
                {
                    array[i] = right[rightIndex];
                    //已经放入了右侧元素进入新数组
                    //所以 标识应该指向下一个
                    rightIndex++;
                }
            }

            //得到了新数组 直接返回出去
            return array;
        }

        //第二步:
        //递归评分数组
        //结束条件为长度小于2
        public static int[] Merge(int[] array)
        {
            //递归结束条件
            if (array.Length < 2)
            {
                return array;
            }

            //1.数组分两段
            int mid = array.Length / 2;

            //2.初始化左右数组
            //左数组
            int[] left = new int[mid];
            //右数组
            int[] right = new int[array.Length - mid];
            //左右初始化内容
            for (int i = 0; i < array.Length; i++)
            {
                if (i < mid)
                {
                    left[i] = array[i];
                }
                else
                {
                    right[i - mid] = array[i];
                }
            }

            //递归再分再排序 先调用Merge
            return Sort(Merge(left), Merge(right));
        }
        #endregion
    }
}

四、快速排序(Quick Sort)

1. 基本思想

  • 选择一个 基准值(pivot)
  • 小于基准的放左边
  • 大于基准的放右边
  • 递归处理左右区间

2. 关键步骤

  1. 选择基准
  2. 左右指针向中间移动
  3. 交换不符合条件的元素
  4. 基准归位
  5. 递归左右区间

3. 时间 & 空间复杂度

情况 复杂度
平均情况 O(n log n)
最坏情况 O(n²)
空间复杂度 O(log n)(递归栈)
稳定性 不稳定

4. 工程实践说明

  • 实际项目中常用

  • 通常会:

    • 随机选基准
    • 三数取中
  • 避免退化为 O(n²)

5. 代码实现

namespace 快速排序
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("快速排序");
            int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
            QuickSort(arr, 0, arr.Length - 1);
            for (int i = 0; i < arr.Length; i++)
            {
                Console.WriteLine(arr[i]);
            }
        }
        #region 知识点一 快速排序基本原理
        //选取基准
        //产生左右标识
        //左右比基准
        //满足则换位

        //排完一次
        //基准定位

        //左右递归
        //直到有序
        #endregion

        #region 知识点二 代码实现
        //第一步
        //申明用于快速排序的函数
        public static void QuickSort(int[] array, int left, int right)
        {
            //第七步:
            //递归函数结束条件
            if (left >= right)
            {
                return;
            }
            //第二步:
            int tempLeft, tempRight, temp;
            //记录基准值
            temp = array[left];
            //左游标
            tempLeft = left;
            //右游标
            tempRight = right;
            //第三步:
            //核心交换逻辑
            //左右游标会不同变化 要不相同时才能继续变化
            while (tempLeft != tempRight)
            {
                //第四步:比较位置交换
                //首先从右边开始 比较 看值有没有资格放到标识的右侧
                while (tempLeft < tempRight && array[tempRight] > temp)
                {
                    tempRight--;
                }
                //移动结束证明可以换位置
                array[tempLeft] = array[tempRight];

                //上面是移动右侧游标
                //接着移动完右侧游标 就要来移动左侧游标
                while (tempLeft < tempRight && array[tempLeft] < temp)
                {
                    tempLeft++;
                }
                //移动结束证明可以换位置
                array[tempRight] = array[tempLeft];
            }
            //第五步:放置基准值
            //跳出循环后 把基准值放在中间位置
            //此时tempRight和tempLeft一定是相等的
            array[tempLeft] = temp;
            //第六步:
            //递归继续
            QuickSort(array, left, tempLeft - 1);
            QuickSort(array, tempRight + 1, right);
        }
        #endregion
    }
}

五、堆排序(Heap Sort)

1. 基本思想

  • 利用 完全二叉树
  • 构建 大顶堆
  • 堆顶元素与末尾交换
  • 调整堆结构
  • 重复直到有序

2. 核心规则

  • 父节点 i
  • 左子节点 2i + 1
  • 右子节点 2i + 2
  • 最大非叶子节点:n / 2 - 1

3. 时间 & 空间复杂度

项目 复杂度
最好 / 平均 / 最坏 O(n log n)
空间复杂度 O(1)
稳定性 不稳定

4. 特点

  • 原地排序
  • 性能稳定
  • 不依赖递归
  • 常用于对空间要求严格的系统

5. 代码实现

using System;
using System.Reflection;

namespace 堆排序
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("堆排序");
            int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
            HeapSort(arr);
            for (int i = 0; i < arr.Length; i++)
            {
                Console.WriteLine(arr[i]);
            }
        }
        #region 知识点一堆排序基本原理
        //构建二叉树
        //大堆顶调整
        //堆顶往后方
        //不停变堆顶

        //关键规则
        //最大非叶子节点:
        //数组长度/2-1

        //父节点和叶子节点:
        //父节点为i
        //左节点21+1
        //右节点2i+2
        #endregion

        #region 知识点二 代码实现
        //第一步:实现父节点和左右节点比较
        /// <summary>
        /// 
        /// </summary>
        /// <param name="array">需要排序的数组</param>
        /// <param name="nowIndex">当前作为根节点的索引</param>
        /// <param name="arrayLength">哪些位置没有确定</param>
        static void HeapCompare(int[] array, int nowIndex, int arrayLength)
        {
            //通过传入的索引 得到它对应的左右叶子节点的索引
            //可能算出来的会溢出数组的索引 我们一会再判断
            int left = 2 * nowIndex + 1;
            int right = 2 * nowIndex + 2;
            //用于记录较大数的索引
            int biggerIndex = nowIndex;
            //先比左 再比右
            //不能溢出
            if (left < arrayLength && array[left] > array[biggerIndex])
            {
                //认为目前最大的是左节点 记录索引
                biggerIndex = left;
            }
            if (right < arrayLength && array[right] > array[biggerIndex])
            {
                //认为目前最大的是右节点 记录索引
                biggerIndex = right;
            }
            if (biggerIndex != nowIndex)
            {
                int temp = array[nowIndex];
                array[nowIndex] = array[biggerIndex];
                array[biggerIndex] = temp;

                //通过递归 看是否影响了叶子节点他们的三角关系
                HeapCompare(array, biggerIndex, arrayLength);
            }
        }
        //第二步:构建大堆顶
        static void BuildBigHeap(int[] array)
        {
            //从最大的非叶子节点索引 开始 不停的往前 去构建大堆顶
            for (int i = array.Length / 2 - 1; i >= 0; i--)
            {
                HeapCompare(array, i, array.Length);
            }
        }
        //第三步:结合大堆顶和节点比较 实现堆排序 把堆顶不停往后移动
        static void HeapSort(int[] array)
        {
            //构建大堆顶
            BuildBigHeap(array);
            //执行过后
            //最大的数肯定就在最上层

            //往屁股后面放 得到 屁股后面最后一个索引
            for (int i = array.Length - 1; i >= 0; i--)
            {
                int temp = array[0];
                array[0] = array[i];
                array[i] = temp;

                //重新进行大堆顶调整
                HeapCompare(array, 0, i);
            }
        }
        #endregion

        #region 知识点三 总结
        //基本原理
        //构建二叉树
        //大堆顶调整
        //堆顶往后方
        //不停变堆顶

        //套路写法
        //3个函数
        //1个堆顶比较
        //1个构建大堆顶
        //1个堆排序

        //重要规则
        //最大非叶子节点索引:
        //数组长度/2-1

        //父节点和叶子节点索引:
        //父节点为i
        //左节点2i+1
        //右节点2i+2

        //注意:
        //堆是一类特殊的树
        //堆的通用特点就是父节点会大于或小于所有子节点
        //我们并没有真正的把数组变成堆
        //只是利用了堆的特点来解决排序问题
        #endregion
    }
}

六、排序算法对比总结

算法 时间复杂度 空间复杂度 稳定性 特点
插入排序 O(n²) O(1) 稳定 小数据、基本有序
希尔排序 ~O(n²) O(1) 不稳定 插入排序优化
归并排序 O(n log n) O(n) 稳定 大数据、性能稳定
快速排序 平均 O(n log n) O(log n) 不稳定 实际最快
堆排序 O(n log n) O(1) 不稳定 空间友好

下面是一份可直接追加到你文档末尾的“概念补充说明”章节
我按面试 / 教学 / 自学三者都通用的标准表述来写,语言与你正文风格保持一致,可原样使用。


七、对比

算法 平均复杂度 最坏复杂度 空间复杂度 稳定性 适用场景
插入排序 O(n²) O(n²) O(1) 小规模数据,几乎有序数据
归并排序 O(n log n) O(n log n) O(n) 需要稳定排序,大数据外部排序
快速排序 O(n log n) O(n²) O(log n) 大规模随机数据,平均性能优先
堆排序 O(n log n) O(n log n) O(1) Top K、优先队列、内存有限、最坏性能稳定

八、常见排序相关概念补充说明

1️⃣ 什么是排序算法的「稳定性」

定义

稳定排序
若待排序序列中存在值相等的元素,排序后它们的相对顺序保持不变,则称该排序算法是稳定的。

举例说明

原始数据(元素含编号):

(5a), (3), (5b), (2)

排序后如果是:

(2), (3), (5a), (5b)

稳定
如果是:

(2), (3), (5b), (5a)

不稳定

稳定性的意义

  • 多关键字排序(先按 A 排,再按 B 排)
  • 数据库排序
  • 业务数据(时间、ID 等隐含顺序)

常见排序算法稳定性

算法 稳定性
插入排序 稳定
希尔排序 不稳定
归并排序 稳定
快速排序 不稳定
堆排序 不稳定

2️⃣ 什么是外部排序(External Sorting)

定义

外部排序
当待排序数据量大到无法一次性全部加载进内存时,
需要借助 磁盘 / SSD 等外部存储 完成的排序过程。

核心特点

  • 数据主要存放在外存
  • 排序过程以顺序读写为主
  • 极少随机访问

典型实现方式

外部排序几乎都采用:

分块排序 + 多路归并
即:

  1. 分批读入内存排序
  2. 将多个有序文件进行归并
    👉 归并排序是外部排序的核心算法

实际应用

  • 数据库 ORDER BY
  • 海量日志排序
  • 搜索引擎索引构建

3️⃣ 什么是链表排序(Linked List Sorting)

定义

链表排序
针对 链式存储结构(单链表 / 双链表) 进行的排序。

链表的限制

  • ❌ 无法随机访问
  • ❌ 无法通过下标直接定位
  • ✅ 只能顺序遍历
  • ✅ 修改指针成本低

为什么常用归并排序

  • 归并排序只需要顺序访问
  • 合并过程只改变 next 指针
  • 时间复杂度稳定 O(n log n)
    👉 链表排序的最优通用解法:归并排序

4️⃣ 为什么“传入的数组不算空间复杂度”

空间复杂度的定义

空间复杂度衡量的是:
算法运行过程中,额外申请的辅助空间大小

不计入空间复杂度的内容

  • 函数参数
  • 原始输入数组
  • 固定数量的局部变量

举例

void Sort(int[] arr)
  • arr调用者提供的
  • 排序算法并未新申请
  • 👉 不计入空间复杂度

什么时候才算

int[] temp = new int[n];
  • 新申请了与 n 成正比的空间
  • 👉 空间复杂度至少 O(n)

5️⃣ 为什么递归层数常是 log n

归并排序 / 快速排序(理想情况)为例:

  • 每一层递归把问题规模减半
  • 递归层数满足:
n → n/2 → n/4 → ... → 1

这是一个 对数关系

递归深度 ≈ log₂ n
👉 因此:

  • 递归栈空间复杂度通常为 O(log n)
  • 不是每次递归都是 log n,而是递归“层数”是 log n

6️⃣ 为什么“排序所有元素”的复杂度是 n

在每一层排序过程中:

  • 每个元素都至少被:
    • 比较一次
    • 或移动一次
      因此:

单层处理成本是 O(n)
结合递归:

  • 每层 O(n)
  • 层数 O(log n)
    得到:
总时间复杂度 = O(n log n)

这是归并排序、理想快排的核心推导逻辑。


7️⃣ 什么是栈递归(递归栈)

定义

栈递归指:
函数在尚未返回时再次调用自身(或同类函数),
导致多个函数调用状态同时存在于调用栈(Stack)中。

为什么需要栈

每一层递归都需要保存:

  • 参数
  • 局部变量
  • 返回地址
    这些信息只能存放在栈内存中。

哪些排序有栈递归

算法 是否产生递归栈
插入 / 希尔
归并排序
快速排序
堆排序 否(本质迭代)

8️⃣ 一句话总总结(建议记忆)

  • 稳定性:相等元素顺序是否保持
  • 外部排序:数据放不进内存,核心是归并
  • 链表排序:无法随机访问,首选归并
  • 原数组不算空间复杂度
  • 递归层数通常是 log n
  • 每层处理所有元素是 n
  • 栈递归来自“未完成函数调用的等待”

posted @ 2025-12-25 11:18  高山仰止666  阅读(18)  评论(0)    收藏  举报