算法实战——排序
一、小范围排序
已知一个几乎有序的数组(几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小),请选择一个合适的排序算法针对这个数组进行排序。
1.分析
(1)时间复杂度为O(n)的算法:计数排序和基数排序
由于我们不知道这个数组的具体范围,故无法使用桶排序思想(不适用所有范围)。
(2)时间复杂度为O(n^2)的算法:冒泡排序、选择排序、插入排序
对于冒泡排序、选择排序:这两种算法的排序与数组的原始序列无关,因此时间复杂度仍为O(n^2)。
对于插入排序:由于每个元素移动的距离可以不超过k,因此在该问题中插入排序的每个元素向前插入的位置不会超过k,因此时间复杂度下降为O(n*k)。
(3)时间复杂度为O(n*log n)的算法:快速排序、归并排序、堆排序
对于快速排序、归并排序:这两种算法的排序与数组的原始序列无关,因此时间复杂度仍为O(n*log n)。
堆排序(改进):每个元素移动的距离可以不超过k,因此数组的最小值一定在array[0] ~ array[k-1]中。因此以array[0] ~ array[k-1]建立小顶堆,然后将堆顶元素赋值给array[0],然后从数组中插入array[k]到堆顶,重建小顶堆。如此反复,直到所有元素都插入到小顶堆中。最后数组中的元素插入完后(还剩堆中的k个元素),则将剩余堆中的元素按照堆排序的方式排序即可。对于每个数进入小顶堆后都要进行时间复杂度O(log k)的建堆过程,然后一共要排序n个元素,因此时间复杂度为O(n*log k)。
2.总结
算法 | 时间复杂度 |
---|---|
计数排序、基数排序 | O(n) 但是该问题未给出数据范围故不适用所有情况,贸然使用可能导致极大的空间复杂度 |
冒泡排序、选择排序 | O(n^2) |
插入排序 | O(n*k) |
归并排序、快速排序 | O(n*logn) |
堆排序 | O(n*log k) |
import java.util.*; public class ScaleSort { public int[] sortElement(int[] A, int n, int k) { // write code here if(A == null || n <= 1) return A; int[] heap = getHeapK(A, k); //获取辅助小顶堆 //每次将堆顶元素放置于数组前部,并将数组的下一个元素放入堆顶,进行堆调整,如此反复 for(int i = k; i < n; i++){ A[i - k] = heap[0]; heap[0] = A[i]; adjustMinHeap(heap, 0, k); } //对剩余的k个元素进行堆排序,每次将堆顶元素置于数组前部 for(int i = n-k; i < n; i++){ A[i] = heap[0]; swap(heap, 0, k-1); adjustMinHeap(heap, 0, --k); } return A; } private int[] getHeapK(int[] A, int k){ //对前k个元素建立小顶堆 int[] heap = new int[k]; for(int i = 0; i < k; i++){ heap[i] = A[i]; } for(int i = k/2 - 1; i >= 0; i--){ adjustMinHeap(heap, i, k); } return heap; } private void adjustMinHeap(int[] heap, int current, int length){ int temp = heap[current]; for(int i = current*2+1; i < length; i = i*2+1){ if(i+1 < length && heap[i+1] < heap[i]) i++; if(temp > heap[i]){ heap[current] = heap[i]; current = i; } else{ break; } } heap[current] = temp; } private void swap(int[] A, int index1, int index2){ int temp = A[index1]; A[index1] = A[index2]; A[index2] = temp; } }
二、低空间复杂度重复值判断
请设计一个高效算法,判断数组中是否有重复值。必须保证额外空间复杂度为O(1)。
本题如果没有空间复杂度限制,应使用哈希表实现,统计每个元素的个数,此时时间复杂度和空间复杂度均为O(n)。
本题的解法为先排序,后遍历,遍历到相同元素说明有重复值,否则没有。因此问题转为应该使用何种排序算法。
在排序算法中,我们知道堆排序的在空间复杂度符合要求O(1)的情况下时间复杂度相对较低,因此本题应使用堆排序(但要注意要非递归实现,否则空间复杂度就不是O(1)了)。
import java.util.*; public class Checker { public boolean checkDuplicate(int[] a, int n) { if(a == null || n <= 1){ return false; } //堆排序 for(int i = n/2-1; i >=0; i--){ adjustMaxHeap(a, i, n); } for(int i = n-1; i > 0; i--){ swap(a, 0, i); adjustMaxHeap(a, 0, i); } //查重 for(int i = 1; i < n; i++){ if(a[i] == a[i-1]) return true; } return false; } private void adjustMaxHeap(int[] a, int current, int length){ int temp = a[current]; for(int i = current*2 + 1; i < length; i = i*2+1){ if(i+1 < length && a[i+1] > a[i]) i++; if(temp < a[i]){ a[current] = a[i]; current = i; } else{ break; } } a[current] = temp; } private void swap(int[] a, int index1, int index2){ int temp = a[index1]; a[index1] = a[index2]; a[index2] = temp; } }
三、有序数组合并
有两个从小到大排序以后的数组A和B,其中A的末端有足够的缓冲空容纳B。请编写一个方法,将B合并入A并排序。
这道题非常简单,对两个数组分别从后向前遍历,每次比较两个指针指向的元素,将较大者至于A数组末尾(下一次放在末尾的前一个位置,如此反复)。最后所有数组遍历完,返回A即可。
import java.util.*; public class Merge { public int[] mergeAB(int[] A, int[] B, int n, int m) { // write code here int indexA = n-1; int indexB = m-1; int mergeIndex = m+n-1; while(indexA >= 0 && indexB >= 0){ A[mergeIndex--] = A[indexA] > B[indexB] ? A[indexA--] : B[indexB--]; } while(indexB >= 0){ A[mergeIndex--] = B[indexB--]; } return A; } }