十大排序算法
1. 常用排序
1.1 归并排序
归并排序(Merge Sort)是一种基于分治策略的排序算法,它的主要思想是将一个未排序的数组划分为两个子数组,分别对这两个子数组进行排序,然后再将排好序的子数组合并成一个有序数组。归并排序的关键步骤在于"合并"操作,这是通过将两个已排序的子数组按照顺序合并而实现的。
归并排序的过程可以分为以下几个步骤:
-
分解:将待排序的数组分成两个子数组,通常是将数组分成相等大小的两部分,直到每个子数组的大小为1或0为止,因为一个元素的数组是有序的。
-
排序:对每个子数组进行递归地排序。这个步骤通过将问题逐步分解为更小的子问题,直到达到基本情况(即单个元素的数组),然后逐步合并子问题的解来实现排序。
-
合并:将已排序的子数组合并为一个有序数组。这是归并排序的核心步骤,它通过比较两个子数组中的元素,并按照顺序将它们合并到一个新的数组中,直到所有元素都被合并。
归并排序具有稳定性,即相等元素的顺序在排序后仍然保持不变。其时间复杂度为O(NlogN),其中N是数组的大小,这使得归并排序在大规模数据集上表现出色。然而,它需要额外的空间来存储临时数组,所以空间复杂度为O(N)。
public static void process(int[] arr, int L, int R) { if (L == R) { return; } int mid = L + ((R - L) >> 1); //递归过程将数组分成树的结构,可画图演示 process(arr, L, mid); process(arr, mid + 1, R); //将树的节点合并 merge(arr, L, mid, R); } private static void merge(int[] arr, int l, int mid, int r) { int[] help = new int[r - l + 1]; int i = 0; int p = l;//左侧 int q = mid + 1;//右侧 while (p <= mid && q <= r) { //相等元素优先合并左侧节点,保证稳定性 help[i++] = arr[p] <= arr[q] ? arr[p++] : arr[q++]; } while (p <= mid) { help[i++] = arr[p++]; } while (q <= r) { help[i++] = arr[q++]; } //辅助数组,保存归并结果 for (i = 0; i < help.length; i++) { arr[l + i] = help[i]; } }
1.2 快速排序
快速排序(Quick Sort)是一种常用的高效的排序算法,它是一种基于比较的排序算法,利用了分治法的思想。快速排序的主要思想是选择一个基准元素,将数组分成两个子数组,一个子数组的所有元素都小于基准元素,另一个子数组的所有元素都大于基准元素,然后递归地对这两个子数组进行排序,最后将它们合并起来。
快速排序的基本步骤如下:
-
选择基准元素:从数组中选择一个元素作为基准(pivot)元素。选择合适的基准元素可以影响算法的性能。
-
划分:将数组中的其他元素与基准元素进行比较,将小于基准元素的元素放在左边,大于基准元素的元素放在右边。最终基准元素将位于其正确的位置,左边是小于它的元素,右边是大于它的元素。
-
递归:递归地对左右两个子数组进行快速排序。
快速排序的平均时间复杂度为O(NlogN),其中N是待排序数组的长度。然而,在最坏情况下,如果选择的基准元素始终是数组中的最小或最大元素,快速排序的时间复杂度可能达到O(N^2),但这种情况相对较少出现。快速排序的实际性能通常比其他基于比较的排序算法(如归并排序和堆排序)好,尤其在大规模数据集上。
private int Partition(int A[], int low, int high) { int pivot = A[low]; while (low < high) { while (low < high && A[high] >= pivot) { high--; } A[low] = A[high]; // 比枢轴元素小的元素移动到左端 while (low < high && A[low] <= pivot) { low++; } A[high] = A[low]; // 比枢轴元素大的元素移动到右端 } A[low] = pivot; // 枢轴元素放到最终位置 return low; // 枢轴元素的最终位置,左边元素小于枢轴元素,右边元素大于枢轴元素 } public void QuickSort(int A[], int low, int high) { if (low < high) { int pivotPos = Partition(A, low, high); // 进行一次划分 QuickSort(A, low, pivotPos - 1); // 递归划分左边部分 QuickSort(A, pivotPos + 1, high); // 递归划分右边部分 } }
快速排序优化
双指针(三向划分)快速排序,选择基准元素(随机化,三数取中法),小规模子数据使用插入排序。
在快速排序中,如果每次选择基准元素都是数组中的最小或最大元素,那么每次分割都会将数组分成一个元素和 n-1 个元素的子数组,导致算法的时间复杂度退化为 O(n^2)。随机选择基准元素可以降低这种最坏情况发生的概率,从而避免退化。
// 基准元素随机化 class Solution { private Random random = new Random(); public int[] sortArray(int[] nums) { if (nums.length < 1) { return nums; } quickSort(nums, 0, nums.length - 1); return nums; } private void quickSort(int[] nums, int low, int high) { if (low >= high) { return; } int pivot = partition(nums, low, high); quickSort(nums, low, pivot - 1); quickSort(nums, pivot + 1, high); } private int partition(int[] nums, int low, int high) { int randomIndex = low + random.nextInt(high - low + 1); int pivot = nums[randomIndex]; nums[randomIndex] = nums[low]; nums[low] = pivot; while (low < high) { while (low < high && nums[high] >= pivot) { high--; } nums[low] = nums[high]; while (low < high && nums[low] <= pivot) { low++; } nums[high] = nums[low]; } nums[low] = pivot; return low; } }
1.3 堆排序
堆排序(Heap Sort)是一种基于二叉堆数据结构的排序算法,它是一种选择排序的一种改进,具有较好的时间和空间复杂度。堆是一种特殊的完全二叉树,分为最大堆和最小堆两种类型,其中最大堆要求父节点的值大于或等于子节点的值,最小堆要求父节点的值小于或等于子节点的值。
堆排序的基本思想是:首先将待排序的数组构建成一个二叉堆(通常使用最大堆),然后不断地将堆顶元素(即最大元素)与堆的最后一个元素交换,并从堆中移除,然后对剩余的元素重新调整为一个合法的堆,重复这个过程直到堆为空,得到一个有序的数组。
以下是堆排序的关键步骤:
- 构建最大堆:从最后一个非叶子节点开始,自底向上地将数组调整为一个最大堆。
- 交换堆顶元素和末尾元素:将堆顶元素与堆的最后一个元素交换,然后将堆的大小减一。
- 调整堆:从堆顶开始,通过逐级下沉(或上浮)操作将堆重新调整为最大堆。
- 重复步骤 2 和 3,直到堆为空。
2. 低级排序
2.1 冒泡排序
public static void bubbleSort(int[] arr) { if(arr == null || arr.length < 2) { return; } for (int i = arr.length - 1; i > 0; i--) { for (int j = 0; j < i; j++) { if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1); } } } }
2.2 直接插入排序
// 插入排序,比较有效地排序方法 public static void insertionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } for (int i = 1; i < arr.length; i++) { // 使0到i范围内有序 for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } } } // 交换数组中的i, j位置上的元素 private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
2.3 希尔排序
希尔排序(Shell Sort)是一种插入排序的一种改进,它通过将数组分成若干个子序列来进行排序,逐步减小子序列的长度,最终完成排序。希尔排序的核心思想是将间隔较大的元素先排好序,然后逐步减小间隔,直到间隔为1,最后进行一次插入排序。
希尔排序的步骤:
- 选择一个增量序列(也称为间隔序列),常用的增量序列有希尔增量、Hibbard增量等。
- 根据增量序列将数组分成若干个子序列,对每个子序列进行插入排序。
- 逐步减小增量,重复步骤 2,直到增量为1,此时进行一次完整的插入排序,完成排序。
希尔增量:最坏O(N^2)平均O(N^1.3)~O(N^1.5)
Hibbard增量:Hibbard增量是希尔排序中使用的一种增量序列,由Donald Hibbard提出。Hibbard增量序列的计算公式是 h_k = 2^k - 1,其中 k 是增量序列的索引。这意味着第一个增量是1,第二个增量是3,第三个增量是7,以此类推。Hibbard增量序列是递增且不互质的,因此在一些情况下可能不够均匀地进行数组分组。最坏时间复杂度可以到O(N^3/2),平均时间复杂度O(N^5/4)。
Sedgewick增量序列:<br />
2.4 选择排序
public static void selectioinSort(int[] arr) { if(arr == null || arr.length < 2) { return; } for (int i = 0; i < arr.length - 1; i++) { int minIndex = i; for (int j = i + 1; j < arr.length; j++) { minIndex = arr[j] < arr[minIndex] ? j : minIndex; } swap(arr, i, minIndex); } }
3. 基于比较的排序算法时间复杂度下限证明
经过k次比较最多能区分的序列个数是2^k,如果有M种序列,需要logM次比较才能区分出大小。那有N个元素的数组右N!中排序可能,需要logN!次比较才能区分出大小,使用斯特林公式可知最低复杂度是NlogN。
4. 排序算法会出现不稳定的状态原因
比较操作不考虑相等情况: 许多排序算法是基于比较操作的,它们只考虑元素的大小关系,而不考虑相等情况。当两个元素的大小相等时,排序算法可能会交换它们的位置,从而破坏了它们在原始序列中的相对顺序,导致排序结果不稳定。
5. 非比较排序
5.1 计数排序
计数排序适用于待排序元素的范围比较小且非负整数的情况。它的基本思想是,统计每个元素出现的次数,然后根据统计信息构建有序的结果序列。
5.2 桶排序
桶排序适用于待排序元素服从均匀分布的情况,它将待排序元素划分为一定数量的桶,然后对每个桶内的元素进行排序,最后将排序后的桶依次合并成有序的结果。
5.3 基数排序
基数排序(Radix Sort)是一种非比较的排序算法,它适用于整数或字符串等具有固定位数的元素。基数排序的基本思想是将待排序的元素从低位到高位依次进行排序,以实现整体的排序。具体来说,基数排序将元素按照各个位上的值进行桶排序,从最低位到最高位依次进行,最终得到有序的结果。
6. 思考:有没有办法实现在O(1)时间复杂度的排序算法?
可以对有限的数字范围内的元素建立一个大哈希表,直接进行映射。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统