赞
踩
就地性:顾名思义,原地排序通过在原数组上直接操作实现排序,无须借助额外的辅助数组,从而节省内存。通常情况下,原地排序的数据搬运操作较少,运行速度也更快。
稳定性:稳定排序在完成排序后,相等元素在数组中的相对顺序不发生改变。
自适应性:自适应排序的时间复杂度会受输入数据的影响,即最佳时间复杂度、最差时间复杂度、平均时间复杂度并不完全相等。
快速排序(Quick Sort)是一种常用的高效的排序算法,它是一种基于比较的排序算法,利用了分治(自顶向下)的思想。快速排序的主要思想是选择一个基准元素,将数组分成两个子数组,一个子数组的所有元素都小于基准元素,另一个子数组的所有元素都大于基准元素,然后递归地对这两个子数组进行排序,最后将它们合并起来。
快速排序的基本步骤如下:
快速排序的平均时间复杂度为O(N * logN),其中N是待排序数组的长度。然而,在最坏情况下,如果选择的基准元素始终是数组中的最小或最大元素,快速排序的时间复杂度可能达到O(N^2),但这种情况相对较少出现。快速排序的实际性能通常比其他基于比较的排序算法好,尤其在大规模数据集上。
优化方案:选择基准元素(随机化(常用),三数取中法),双指针(三向划分)快速排序,小规模子数据使用插入排序。
在快速排序中,如果每次选择基准元素都是数组中的最小或最大元素,那么每次分割都会将数组分成一个元素和 N - 1 个元素的子数组,导致算法的时间复杂度退化为 O(N^2)。随机选择基准元素可以降低这种最坏情况发生的概率,从而避免退化。
- import java.util.Random;
-
- /**
- * 随机化基准元素优化快速排序
- */
- public class QuickSort {
- private Random random = new Random();
-
- public int[] sortArray(int[] nums) {
- if (nums == null || nums.length < 2) {
- 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;
- }
-
- }
通常快速排序的效率更高,主要有以下原因:
归并排序(Merge Sort)是一种基于分治(自底向上)的排序算法,它的主要思想是将一个未排序的数组划分为两个子数组,分别对这两个子数组进行排序,然后再将排好序的子数组合并成一个有序数组。归并排序的关键步骤在于"合并"操作,这是通过将两个已排序的子数组按照顺序合并而实现的。
归并排序的过程可以分为以下几个步骤:
归并排序具有稳定性,即相等元素的顺序在排序后仍然保持不变。其时间复杂度为O(N * logN),其中N是数组的大小,这使得归并排序在大规模数据集上表现出色。然而,它需要额外的空间来存储临时数组,所以空间复杂度为O(N)。
- /**
- * 归并排序
- */
- public class MergeSort {
-
- public int[] sortArray(int[] nums) {
- if (nums == null || nums.length < 2) {
- return nums;
- }
- mergeSort(nums, 0, nums.length - 1);
- return nums;
- }
-
- private void mergeSort(int[] nums, int left, int right) {
- // 边界情况
- if (left >= right) {
- return;
- }
- int mid = left + (right - left) / 2;
- // 自上而下分治
- mergeSort(nums, left, mid);
- mergeSort(nums, mid + 1, right);
- // 自下而上合并
- mergeArray(nums, left, mid, right);
- }
-
- private void mergeArray(int[] nums, int left, int mid, int right) {
- int[] helperArray = new int[right - left + 1];
- int leftPointer = left;
- int rightPointer = mid + 1;
- int i = 0; // 辅助数组索引
- while (leftPointer <= mid && rightPointer <= right) {
- helperArray[i++] = nums[leftPointer] < nums[rightPointer] ?
- nums[leftPointer++] : nums[rightPointer++];
- }
- while (leftPointer <= mid) {
- helperArray[i++] = nums[leftPointer++];
- }
- while (rightPointer <= right) {
- helperArray[i++] = nums[rightPointer++];
- }
- for (i = 0; i < helperArray.length; i++) {
- nums[left + i] = helperArray[i];
- }
- }
-
- }
堆排序(Heap Sort)是一种基于二叉堆数据结构的排序算法,它是一种选择排序的一种改进,具有较好的时间和空间复杂度。堆是一种特殊的完全二叉树,分为最大堆和最小堆两种类型,其中最大堆要求父节点的值大于或等于子节点的值,最小堆要求父节点的值小于或等于子节点的值。
堆排序的基本思想:首先将待排序的数组构建成一个二叉堆(通常使用最大堆),出元素的时候将堆顶元素(即最大元素)与堆的最后一个元素交换,并从堆中移除,然后对剩余的元素重新调整为一个合法的堆,重复这个过程直到堆为空,得到一个有序的数组。
以下是堆排序的关键步骤:
Poor use of cache memory:平均时间上,堆排序的时间常数比快排要大一些,因此通常会慢一些,但是堆排序最差时间也是O(N * logN),这点比快排好。快排在递归进行部分的排序的时候,只会访问局部的数据,因此缓存能够更大概率的命中;而堆排序的建堆过程是整个数组各个位置都访问到的,后面则是所有未排序数据各个位置都可能访问到的,所以不利于缓存发挥作用。简单的说就是快排的存取模型的局部性更强,堆排序差一些。
- /**
- * 堆排序
- * 非叶子节点:在完全二叉树中,所有非叶子节点的索引都在 0 到 n / 2 - 1 之间,其中 n 是数组的长度。
- * 叶子节点:叶子节点是树的最底层节点,没有子节点。在完全二叉树中,从 n / 2 到 n - 1 的节点都是叶子节点。
- */
- public class HeapSort {
-
- public int[] sortArray(int[] nums) {
- if (nums == null || nums.length < 2) {
- return nums;
- }
- int n = nums.length;
- // 从最后一个非叶子节点自下而上进行调整
- for (int i = n / 2 - 1; i >= 0; i--) {
- heapify(nums, n, i);
- }
-
- for (int i = n - 1; i >= 0; i--) {
- swap(nums, 0, i);
- heapify(nums, i, 0);
- }
-
- return nums;
- }
-
- /**
- * 堆化方法,维护堆的性质
- * @param nums 数组表示的堆
- * @param n 堆的大小
- * @param i 当前需要堆化的节点的索引
- */
- private void heapify(int[] nums, int n, int i) {
- int largest = i;
- int left = 2 * i + 1; // 左子节点的索引
- int right = 2 * i + 2; // 右子节点的索引
- if (left < n && nums[left] > nums[largest]) {
- largest = left;
- }
- if (right < n && nums[right] > nums[largest]) {
- largest = right;
- }
- if (largest != i) {
- swap(nums, largest, i);
- heapify(nums, n, largest);
- }
- }
-
- private void swap(int[] nums, int i, int j) {
- int temp = nums[i];
- nums[i] = nums[j];
- nums[j] = temp;
- }
-
- }
- public class BubbleSort {
-
- // 冒泡排序
- public 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);
- }
- }
- }
- }
-
- }
我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 flag 来监测这种情况,一旦出现就立即返回。
- public class BubbleSort {
-
- public int[] sortArray(int[] nums) {
- if (nums == null || nums.length < 2) {
- return nums;
- }
- boolean flag; // 标志该轮循环数组是否已经有序
-
- for (int i = nums.length - 1; i > 0; i--) {
- flag = true;
- for (int j = 0; j < i; j++) {
- if (nums[j] > nums[j + 1]) {
- swap(nums, i, j);
- flag = false;
- }
- }
- if (flag) {
- break;
- }
- }
-
- return nums;
- }
-
- private void swap(int[] nums, int i, int j) {
- int temp = nums[i];
- nums[i] = nums[j];
- nums[j] = temp;
- }
-
- }
- public class SelectionSort {
-
- public void selectionSort(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);
- }
- }
-
- }
- public class InsertionSort {
-
- // 插入排序,比较有效地排序方法
- public void insertionSort(int[] arr) {
- if (arr == null || arr.length < 2) {
- return;
- }
- for (int i = 1; i < arr.length; i++) {
- // [0, i) 范围内有序,这里的 i 是要比较的元素位置
- for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
- swap(arr, j, j + 1);
- }
- }
- }
-
- }
尽管插入排序的时间复杂度更高,但在数据量较小的情况下,插入排序通常更快(递归的栈空间的消耗)。
插入排序在时间复杂度的常数因素上比冒泡排序和选择排序更小:
希尔排序(Shell Sort)是一种插入排序的一种改进,它通过将数组分成若干个子序列来进行排序,逐步减小子序列的长度,最终完成排序。希尔排序的核心思想是将间隔较大的元素先排好序,然后逐步减小间隔,直到间隔为1,最后进行一次插入排序。
希尔排序的步骤:
希尔增量序列:最希尔增量是最简单的增量序列,其取值为 N / 2,每次递减一半,直到增量为1。其中,N 是数组的长度。最坏时间复杂度 O(N^2),平均时间复杂度 O(N^1.3)~O(N^1.5)。
Hibbard增量序列:Hibbard增量序列采用 2^k - 1(k 为非负整数)作为增量。Hibbard增量的性能较好,但在实际应用中可能并不总是最优。最坏时间复杂度O(N^3/2),平均时间复杂度O(N^5/4)。
Sedgewick增量序列:Sedgewick增量序列通过 4^k + 3 * 2^(k-1) 和 2^(k+2) * (2^(k+2) - 3) + 1 两种增量交替使用,最坏时间复杂度O(N^(4/3)),平均时间复杂度 O(N^(7/6))。
经过k次比较最多能区分的序列个数是2^k,如果有M种序列,需要logM次比较才能区分出大小。那有N个元素的数组有 N! 种排序可能,需要logN!次比较才能区分出大小,使用斯特林公式可知最低复杂度是N * logN。
比较操作不考虑相等情况:许多排序算法是基于比较操作的,它们只考虑元素的大小关系,而不考虑相等情况。当两个元素的大小相等时,排序算法可能会交换它们的位置,从而破坏了它们在原始序列中的相对顺序,导致排序结果不稳定。
计数排序适用于待排序元素的范围比较小且非负整数的情况。它的基本思想是,统计每个元素出现的次数,然后根据统计信息构建有序的结果序列。
桶排序适用于待排序元素服从均匀分布的情况,它将待排序元素划分为一定数量的桶,然后对每个桶内的元素进行排序,最后将排序后的桶依次合并成有序的结果。
桶排序的时间复杂度理论上可以达到 O(N),关键在于将元素均匀分配到各个桶中,因为实际数据往往不是均匀分布的。为实现平均分配,我们可以先设定一条大致的分界线,将数据粗略地分到 3 个桶中。分配完毕后,再将商品较多的桶继续划分为 3 个桶,直至所有桶中的元素数量大致相等。
如果我们提前知道商品价格的概率分布,则可以根据数据概率分布设置每个桶的价格分界线。值得注意的是,数据分布并不一定需要特意统计,也可以根据数据特点采用某种概率模型进行近似。
基数排序是一种非比较的排序算法,它适用于整数或字符串等具有固定位数的元素。基数排序的基本思想是将待排序的元素从低位到高位依次进行排序,以实现整体的排序。具体来说,基数排序将元素按照各个位上的值进行桶排序,从最低位到最高位依次进行,最终得到有序的结果。
可以对有限的数字范围内的元素建立一个大哈希表,直接进行映射。
数组长度 | 所使用的排序算法 |
length < 47 | 插入排序 |
47 <= length < 286 | 快速排序 |
length >= 286 且数组基本有序 | 归并排序 |
length >= 286 且数组基本无序 | 快速排序 |
基本有序的判断方法:逆序对计数法。
Google 之所以能够实现如此快速的搜索结果排序,是因为其搜索引擎背后运用了一系列高效的技术和算法。以下是一些关键因素:
需要注意的是,Google 搜索引擎的高速度是整个系统综合运作的结果,包括硬件、分布式架构、算法优化等多个方面。这种高效性是通过多年来不断的研究和工程实践来实现的。
外部排序算法用于处理不能完全装入内存的大型数据集。由于数据量超出了内存容量,需要借助外存(如磁盘)进行排序。外部排序算法的主要思想是将数据分成适当大小的块,逐块排序后再进行合并。以下是外部排序的基本步骤:
分块:假设有一个非常大的数据集,需要对其进行排序,但内存有限。首先,将数据集分成若干个块,每个块的大小应能在内存中完全处理。
多路归并排序:假设已经有n个排序后的数据块,现在需要将它们合并成一个整体的有序数据集。这一步使用多路归并排序来完成。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。