八大排序算法
名称 | 数据对象 | 稳定性 | 时间复杂度 | 空间复杂度 | 描述 |
---|---|---|---|---|---|
冒泡排序 | 数组 | 稳定 | \(O(n^2)\) | \(O(1)\) | |
选择排序 | 数组、链表 | 不稳定/稳定 | \(O(n^2)\) | \(O(1)\) | 找到最小元素的下标,依次向前部放置 |
插入排序 | 数组、链表 | 稳定 | \(O(n^2)\) | \(O(1)\) | 扑克牌插法 |
希尔排序 | 数组 | 不稳定 | \(O(n\log n)\) | \(O(1)\) | 每次按照事先的间隔进行插入排序,间隔依次减小,最后一次一定为1 |
归并排序 | 数组、链表 | 稳定 | \(O(n\log n)\) | 向上:\(O(1)\);向下\(O(n)\) | 向下的空间复杂度因为 有一个tempArray 所以不是\(O(\log n)\)是\(O(n)\) |
快速排序 | 数组 | 不稳定 | \(O(n\log n)\) | \(O(\log n)\) | |
堆排序 | 数组 | 不稳定 | \(O(n\log n)\) | \(O(1)\) | |
计数排序 | 数组、链表 | 稳定 | \(O(n+m)\) | \(O(n+m)\) | 相对于比较排序速度快得多, 但是空间消耗较大。当存在负数的时候,需要将其整体置为正数。 |
桶排序 | 数组、链表 | 稳定 | \(O(n)\) | \(O(m)\) | bucketSize 为1的时候,直接退化为计数排序 |
基数排序 | 数组、链表 | 稳定 | \(O(k\times n)\) | \(O(m)\) | |
败者树 | 数组、链表 |
分类
排序算法可以分为内部排序和外部排序,内部排序是指数据记录再内存中进行排序,而外部排序则是因为数据量非常大,无法一次读入内存,再排序的工程中经常需要访问外存。
常见的内部排序算法:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
冒泡排序
public class Solution {
public int[] bubbleSort(int[] array) {
int len = array.length;
int[] arr = Arrays.copyOf(array, len);
for (int i = 1; i < len; i++) {
boolean flag = true;
for (int j = 0; j < len - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
flag = false;
}
}
if (flag) {
break;
}
}
return arr;
}
private void swap(int[] array, int x, int y) {
int temp = array[x];
array[x] = array[y];
array[y] = temp;
}
}
选择排序
选择排序也可以对链表进行操作,具体操作步骤和数组的一致。
public class Solution {
public int[] selectionSort(int[] array) {
int len = array.length;
int[] arr = Arrays.copyOf(array, len);
for (int i = 0; i < len; i++) {
int minIndex = i;
for (int j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (i != minIndex) {
swap(arr, i, minIndex);
}
}
return arr;
}
private void swap(int[] array, int x, int y) {
int temp = array[x];
array[x] = array[y];
array[y] = temp;
}
}
插入排序
public class Solution {
public int[] insertionSort(int[] array) {
int len = array.length;
int[] arr = Arrays.copyOf(array, len);
for (int i = 1; i < len; i++) {
int temp = arr[i];
int j = i;
while (j > 0 && temp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
if (j != i) {
arr[j] = temp;
}
}
return arr;
}
}
希尔排序
其成为递减增量排序算法,是插入排序的一种更高效的版本,
- 插入排序对已经已经有序的数组进行排序的时候,效率可以达到线性排序的效果。
希尔排序的基本思想是:先将整个待排序的序列分割为若干子序列进行插入排序,待整个序列中的记录基本有序的时候,再对全体记录依次进行直接插入排序。
public class Solution {
public int[] shellSort(int[] array) {
int len = array.length;
int[] arr = Arrays.copyOf(array, len);
for (int step = len / 2; step >= 1; step /= 2) {
for (int i = step; i < len; i++) {
int temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}
return arr;
}
}
归并排序
自顶向下的方法/递归法
空间复杂度:此处的merge
方法,因为其中的tempArray
,所以空间复杂度应该为\(O(\log n+n)\),所以此处时间复杂度为\(O(n)\)。
时间复杂度:因为merge
的时间复杂度为\(O(n)\),splitArray
需要调用\(\log n\)次merge
,所以此处时间复杂度为\(O(n\log n)\)
public class Solution {
public int[] mergeSort(int[] array) {
int len = array.length;
int[] arr = Arrays.copyOf(array, len);
splitArray(arr, 0, arr.length);
return arr;
}
private void splitArray(int[] array, int start, int end) {
if (end - start < 2) {
return;
}
int middle = (start + end) / 2;
splitArray(array, start, middle);
splitArray(array, middle, end);
merge(array, start, middle, end);
}
private void merge(int[] array, int start, int middle, int end) {
int i = 0, j = 0, tempIndex = 0;
int[] tempArray = new int[end - start];
while (i + start < middle && j + middle < end) {
if (array[start+i] < array[middle+j]) {
tempArray[tempIndex] = array[start+i];
tempIndex++;
i++;
} else {
tempArray[tempIndex] = array[middle+j];
tempIndex++;
j++;
}
}
while (j+middle<end){
tempArray[tempIndex] = array[j+middle];
j++;
tempIndex++;
}
while (start+i<middle){
tempArray[tempIndex] = array[start+i];
i++;
tempIndex++;
}
for(i=start;i<end;i++){
array[i] = tempArray[i-start];
}
}
}
自下向上的方法/迭代法
public class Solution {
public int[] mergeSort(int[] array) {
int len = array.length;
int[] arr = Arrays.copyOf(array, len);
int[] tempArray = new int[len];
int leftMin = 0, leftMax = 0, rightMin = 0, rightMax = 0, tempIndex = 0;
for (int i = 1; i < len; i *= 2) {
for (leftMin = 0; leftMin < len-i; leftMin=rightMax) {
leftMax = rightMin = leftMin + i;
rightMax = rightMin + i;
if (rightMax > len) {
rightMax = len;
}
tempIndex = 0;
while (leftMin < leftMax && rightMin < rightMax) {
if (arr[leftMin] < arr[rightMin]) {
tempArray[tempIndex++] = arr[leftMin++];
} else {
tempArray[tempIndex++] = arr[rightMin++];
}
}
while(leftMax-leftMin>0){
tempArray[tempIndex++] = arr[leftMin++];
}
while (rightMax-rightMin>0){
tempArray[tempIndex++] = arr[rightMin++];
}
while (tempIndex>0){
arr[--rightMin] =tempArray[--tempIndex];
}
}
}
return arr;
}
}
快速排序
public class Solution {
public int[] sort(int[] array) {
int len = array.length;
int[] arr = Arrays.copyOf(array, len);
return quickSort(arr, 0, len - 1);
}
private int[] quickSort(int[] arr, int left, int right) {
if (left < right) {
int index = partition(arr, left, right);
quickSort(arr, left, index - 1);
quickSort(arr, index + 1, right);
}
return arr;
}
public int partition(int[] arr, int left, int right) {
Random random = new Random();
swap(arr,left,random.nextInt(left, right + 1));
int temp = arr[left];
while (left < right) {
while (arr[right] >= temp && left < right) {
right--;
}
arr[left] = arr[right];
while (arr[left] < temp && left < right) {
left++;
}
arr[right] = arr[left];
}
arr[left] = temp;
return left;
}
private void swap(int[] nums,int x,int y){
int temp = nums[x];
nums[x] = nums[y];
nums[y] = temp;
}
}
堆排序
public class Solution {
/**
* 大根堆, 每次讲最大的数字放到最后,所以是从小到达排列。
*
* @param array
*/
private void heapSort(int[] array) {
buildHeap(array);
for (int i = array.length - 1; i >= 0; i--) {
swap(array, 0, i);
heapify(array, i, 0);
}
}
private void buildHeap(int[] array) {
int len = array.length;
int toHeapIndex = (len - 2) / 2;
for (int i = toHeapIndex; i >= 0; i--) {
heapify(array, len, i);
}
}
/**
* @param array 需要heapify的数组
* @param len 数组的长度,或者说是需要heapify的长度
* @param i 从i开始进行递归的heapify
*/
private void heapify(int[] array, int len, int i) {
int c1 = 2 * i + 1;
int c2 = 2 * i + 2;
int max = i;
if (c1 < len && array[c1] > array[max]) {
max = c1;
}
if (c2 < len && array[c2] > array[max]) {
max = c2;
}
if (max != i) {
swap(array, max, i);
heapify(array, len, max);
}
}
private void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
计数排序
public class Solution {
public int[] countingSort(int[] array) {
int maxValue = getMaxValue(array);
int[] arr = Arrays.copyOf(array, array.length);
int[] bucketArray = new int[maxValue + 1];
for (int i : arr) {
bucketArray[i]++;
}
int arrayIndex = 0;
for (int i = 0; i < bucketArray.length; i++) {
while (bucketArray[i] > 0) {
arr[arrayIndex++] = i;
bucketArray[i]--;
}
}
return arr;
}
private int getMaxValue(int[] array) {
int maxValue = Integer.MIN_VALUE;
for (int i : array) {
maxValue = Math.max(i, maxValue);
}
return maxValue;
}
}
桶排序
桶排序的话,和计数排序相比,就是多了一个参数,通过该参数指定\(n\)个连续的数字存到一个桶内。数字越小(假如为\(1\)),就直接变成了计数排序。
public class Solution {
public int[] bucketSort(int[] arr, int bucketSize) {
if (arr.length == 0) {
return arr;
}
int minvalue = arr[0], maxValue = arr[0];
for (int i : arr) {
minvalue = Math.min(minvalue, i);
maxValue = Math.max(maxValue, i);
}
int bucketCount = (int) (Math.floor((maxValue - minvalue) / bucketSize) + 1);
int[][] buckets = new int[bucketCount][0];
for (int i = 0; i < arr.length; i++) {
int index = (int) Math.floor((arr[i] - minvalue) / bucketSize);
buckets[index] = arrAppend(buckets[index], arr[i]);
}
int arrIndex = 0;
for (int[] bucket : buckets) {
if(bucket.length<=0){
continue;
}
Arrays.sort(bucket); // 此处使用 插入替换
for (int i : bucket) {
arr[arrIndex++] = i;
}
}
return arr;
}
private int[] arrAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}
基数排序
- 计数排序:每个桶都存储单一的值
- 桶排序:每个桶存放一定范围内的数值
- 基数排序:根据键值的每个数字分配桶
但是这几个都只能对正整数进行排序,前面的比较排序的方法可以对任意数字进行排序。
public class Solution {
public int[] radixSort(int[] array) {
int numLength = getMaxNumLength(array);
int mod = 10;
int dev = 1;
for (int i = 0; i < numLength; i++, dev *= 10, mod *= 10) {
int[][] counter = new int[mod][0];
for (int value : array) {
int bucket = (value % mod) / dev;
counter[bucket] = arrayAppend(counter[bucket], value);
}
int tempIndex = 0;
for (int[] ints : counter) {
for (int anInt : ints) {
array[tempIndex++] = anInt;
}
}
}
return array;
}
private int getMaxNumLength(int[] array) {
int maxValue = array[0];
for (int i : array) {
maxValue = Math.max(maxValue, i);
}
return String.valueOf(maxValue).length();
}
private int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}
败者树
概念
外部排序,就是对外存中的记录进行排序。其主要作用就是将内存作为工作空间来辅助外村数据的排序。
外部排序最常用的算法是归并排序,之所以归并排序常用,是因为它不需要将全部记录读入内存,即可完成排序。因此,可以解决由于内存空间不足导致的无法对大规模记录排序的问题。
假设我们需要对外存中一组大规模的无需记录进行排序,则可以按照下面的步骤进行。
置换-选择排序
假设我们有下面一组再外存中的记录\(\{15,19,04,83,12,27,11,25,16,34,26,07,10,90,06\}\),内存缓冲区可以容纳四个记录,我们需要对其进行生成初始归并段,下面的Gif
展示了如何操作。
通过置换选择排序,我们得到了\(m\)棵长短不一的树,然后我们需要对这些树进行归并排序,由于内存大小的限制,我们最大只能选择\(K\)路归并排序,但是不同的归并顺序会带来不同的\(IO\)次数,此时我们要选择一种最小\(IO\)次数的归并方法,所以就有了下面的最佳归并树。
最佳归并树
假设,我们由选择-置换排序得到了9个初始归并段,其长度(记录数量)依次为\(\{9,30,12,18,3,17,2,6,24\}\),请做出三路归并树,并计算\(IO\)次数。
由此我们可以使用哈夫曼树的构建方法去构建最佳归并树。
\(IO次数 = 2\times(2\times3+3\times3+6\times3+9\times2+12\times2+30\times1+17\times2+18\times2+24\times2)=446\)
此处\(2\times\)的原因的是因\(IO\)是两次,读入一次,写出一次。
败者树
此前,经过置换-选择排序我们得到了初始归并段,通过最佳归并树我们得到了如何选择不同的段进行归并,才能使\(IO\)次数最小。