赞
踩
排序是计算机的核心内容,快速排序算法一直推动计算机的发展,同时也可以为我们工作生活提供一些有益的优化,帮助我们了解管理与社会的本质。
十种常见排序算法可以分为两大类:
非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
术语说明
图片名词解释
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr之前有多少个元素,则唯一确定了arr在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; /** * 冒泡排序 * * @author zzn * @since 2019/10/18 19:53 */ public class BubbleSort implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对原数组进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); for (int i = 1; i < arr.length; i++) { // 设定一个标记,若为 true,则表示此次循环没有进行交换,也就是待排序列已经有序了,那么排序就已经完成。 boolean flag = true; for (int j = 0; j < arr.length - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; flag = false; } } if (flag) { break; } } return arr; } @Test public void testList() { // ArrayList<Integer> list = new ArrayList<>(Arrays.asList(11, 334, 64, 32, 667, 346, 478, 3, 1, 789, 453, 4)); int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 选择排序 * * @author zzn * @since 2019/10/18 21:03 */ public class SelectionSort implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对原数组进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); // 总共要经过 N-1 轮比较 for (int i = 0; i < arr.length; i++) { // 记录最小值的元素下标,默认是 第一个 int min = i; // 每轮需要比较的次数 N-i for (int j = i + 1; j < arr.length; j++) { if (arr[j] < arr[min]) { // 记录目前内找到的最小值的元素下标 min = j; } } // 将找到的最小值和 i 位置所在的值进行交换 if (i != min) { int tmp = arr[i]; arr[i] = arr[min]; arr[min] = tmp; } } return arr; } @Test public void testList() { int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 插入排序 * * @author zzn * @since 2019/10/18 22:50 */ public class InsertSort implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对原数组进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); // 从下标为 1 的元素开始选择合适的位置插入,因为下标为 0 的只有一个元素,默认是有序的 for (int i = 0; i < arr.length; i++) { // 记录要插入的数据 int tmp = arr[i]; // 从已经排序的序列最右边开始比较,找到比其小的数 int j = i; while (j > 0 && tmp < arr[j - 1]) { arr[j] = arr[j - 1]; j--; } // 存在比其小的数,插入 if (j != i) { arr[j] = tmp; } } return arr; } @Test public void testList() { int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 希尔排序 * * @author zzn * @since 2019/10/19 0:16 */ public class ShellSort implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对原数组进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); // 设置增量 int gap = arr.length / 2; while (gap > 0) { for (int i = gap; i < arr.length; i++) { int tmp = arr[i]; int j = i - gap; while (j >= 0 && arr[j] > tmp) { arr[j + gap] = arr[j]; j -= gap; } arr[j + gap] = tmp; } gap = gap / 2; } return arr; } @Test public void testList() { int[] ints = new int[]{9,8,6,4,3,5,0}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 归并排序 * * @author zzn * @since 2019/10/19 6:11 */ public class MergeSort implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对原数组进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); if (arr.length < 2) { return arr; } // 把长度为n的输入序列分成两个长度为n/2的子序列 int middle = arr.length / 2; int[] left = Arrays.copyOfRange(arr, 0, middle); int[] right = Arrays.copyOfRange(arr, middle, arr.length); return merge(sort(left), sort(right)); } private int[] merge(int[] left, int[] right) { int[] result = new int[left.length + right.length]; int i = 0; while (left.length > 0 && right.length > 0) { if (left[0] <= right[0]) { result[i++] = left[0]; left = Arrays.copyOfRange(left, 1, left.length); } else { result[i++] = right[0]; right = Arrays.copyOfRange(right, 1, right.length); } } while (left.length > 0) { result[i++] = left[0]; left = Arrays.copyOfRange(left, 1, left.length); } while (right.length > 0) { result[i++] = right[0]; right = Arrays.copyOfRange(right, 1, right.length); } return result; } @Test public void testList() { int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 快速排序 * * @author zzn * @since 2019/10/19 10:31 */ public class QuickSort implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); return quickSort(arr, 0, arr.length - 1); } private int[] quickSort(int[] arr, int left, int right) { if (left < right) { int partitionIndex = partition(arr, left, right); quickSort(arr, left, partitionIndex - 1); quickSort(arr, partitionIndex + 1, right); } return arr; } private int partition(int[] arr, int left, int right) { // 设定基准值(pivot) int pivot = left; int index = pivot + 1; for (int i = index; i <= right; i++) { if (arr[i] < arr[pivot]) { swap(arr, i, index); index++; } } swap(arr, pivot, index - 1); return index - 1; } private void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } @Test public void testList() { int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 快速排序 2 * 从左边找比基数大的值,从右边找比基数小的值,然后进行交换 * * @author zzn * @since 2019/10/19 12:25 */ public class QuickSort02 implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); quickSort(arr, 0, arr.length - 1); return arr; } private void quickSort(int[] arr, int low, int high) { int left, right, temp, t; if (low > high) return; left = low; right = high; //temp就是基准位 temp = arr[low]; while (left < right) { //先看右边,依次往左递减 while (temp <= arr[right] && left < right) { right--; } //再看左边,依次往右递增 while (temp >= arr[left] && left < right) { left++; } //如果满足条件则交换 if (left < right) { t = arr[right]; arr[right] = arr[left]; arr[left] = t; } } //最后将基准为与i和j相等位置的数字交换 arr[low] = arr[left]; arr[left] = temp; //递归调用左半数组 quickSort(arr, low, right - 1); //递归调用右半数组 quickSort(arr, right + 1, high); } @Test public void testList() { int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 11, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
堆排序的平均时间复杂度为 Ο(nlogn)。
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 堆排序 * * @author zzn * @since 2019/10/19 15:37 */ public class HeapSort implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); int len = arr.length; // 构建一个最大堆 buildMaxHeap(arr, len); // 将最后一个元素和第一个元素的位置进行交换 for (int i = len - 1; i > 0; i--) { swap(arr, 0, i); len--; heapify(arr, 0, len); } return arr; } private void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } /** * 构建一个最大堆 * * @param arr 数组 * @param len 长度 */ private void buildMaxHeap(int[] arr, int len) { for (int i = len / 2; i >= 0; i--) { heapify(arr, i, len); } } private void heapify(int[] arr, int i, int len) { // 左子树 int left = 2 * i + 1; // 右子树 int right = 2 * i + 2; // 父节点 int largest = i; // 如果有左子树,且左子树的值大于父节点,则将最大指针指向左子树 if (left < len && arr[left] > arr[largest]) { largest = left; } // 如果有右子树,且右子树的值大于父节点,则将最大指针指向右子树 if (right < len && arr[right] > arr[largest]) { largest = right; } // 如果父节点不是最大值,则将父节点与最大值交换,并且递归调整父节点交换的位置 if (largest != i) { swap(arr, i, largest); heapify(arr, largest, len); } } @Test public void testList() { int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 11, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
\1. 计数排序的特征
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 计数排序 * * @author zzn * @since 2019/10/19 16:02 */ public class CountingSort implements IArraySort { @Override public int[] sort(int[] sourceArray) { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); int maxValue = getMaxValue(arr); return countingSort(arr, maxValue); } private int[] countingSort(int[] arr, int maxValue) { int bucketLen = maxValue + 1; int[] bucket = new int[bucketLen]; for (int value : arr) { bucket[value]++; } int sortedIndex = 0; for (int j = 0; j < bucketLen; j++) { while (bucket[j] > 0) { arr[sortedIndex++] = j; bucket[j]--; } } return arr; } private int getMaxValue(int[] arr) { int maxValue = arr[0]; for (int value : arr) { if (maxValue < value) { maxValue = value; } } return maxValue; } @Test public void testList() { int[] ints = new int[]{1,2,3,4,5,6,7,8,9,1,2,4,5,1,4,6,8,7,2,4,6,4,5,4,2,1,6,4,8,7}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
元素分布在桶中:
然后,元素在每个桶中排序:
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 桶排序 * * @author zzn * @since 2019/10/19 17:03 */ public class BucketSort implements IArraySort { private static final InsertSort insertSort = new InsertSort(); @Override public int[] sort(int[] sourceArray) { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); return bucketSort(arr, 5); } private int[] bucketSort(int[] arr, int bucketSize) { if (arr.length == 0) { return arr; } int minValue = arr[0]; int maxValue = arr[0]; for (int value : arr) { if (value < minValue) { minValue = value; } else if (value > maxValue) { maxValue = value; } } int bucketCount = (maxValue - minValue) / bucketSize + 1; int[][] buckets = new int[bucketCount][0]; // 利用映射函数将数据分配到各个桶中 for (int i = 0; i < arr.length; i++) { int index = (arr[i] - minValue) / bucketSize; buckets[index] = arrAppend(buckets[index], arr[i]); } int arrIndex = 0; for (int[] bucket : buckets) { if (bucket.length <= 0) { continue; } // 对每个桶进行排序,这里使用了插入排序 bucket = insertSort.sort(bucket); for (int value : bucket) { arr[arrIndex++] = value; } } return arr; } /** * 自动扩容,并保存数据 * * @param arr * @param value */ private int[] arrAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = value; return arr; } @Test public void testList() { int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 11, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
package com.zzn.sort.impl; import com.zzn.sort.IArraySort; import org.junit.Test; import java.util.Arrays; /** * 基数排序 * 考虑负数的情况还可以参考: https://code.i-harness.com/zh-CN/q/e98fa9 * * @author zzn * @since 2019/10/19 18:56 */ public class RadixSort implements IArraySort { @Override public int[] sort(int[] sourceArray){ // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); int maxDigit = getMaxDigit(arr); return radixSort(arr, maxDigit); } /** * 获取最高位数 */ private int getMaxDigit(int[] arr) { int maxValue = getMaxValue(arr); return getNumLenght(maxValue); } private int getMaxValue(int[] arr) { int maxValue = arr[0]; for (int value : arr) { if (maxValue < value) { maxValue = value; } } return maxValue; } protected int getNumLenght(long num) { if (num == 0) { return 1; } int lenght = 0; for (long temp = num; temp != 0; temp /= 10) { lenght++; } return lenght; } private int[] radixSort(int[] arr, int maxDigit) { int mod = 10; int dev = 1; for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) { // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10) int[][] counter = new int[mod * 2][0]; for (int j = 0; j < arr.length; j++) { int bucket = ((arr[j] % mod) / dev) + mod; counter[bucket] = arrayAppend(counter[bucket], arr[j]); } int pos = 0; for (int[] bucket : counter) { for (int value : bucket) { arr[pos++] = value; } } } return arr; } /** * 自动扩容,并保存数据 * * @param arr * @param value */ private int[] arrayAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = value; return arr; } @Test public void testList() { int[] ints = new int[]{11, 334, 64, 32, 667, 346, 478, 11, 3, 1, 789, 453, 4}; int[] sort = sort(ints); System.out.println("sort = " + Arrays.toString(sort)); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。