排序作爲CS的基本功,需要單獨拿出來總結一下。
這是一個直觀地可以觀看各種排序算法的可視化效果的網址(強烈推薦):http://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html
我們先來回顧一下幾種基本的排序算法,時間複雜度爲O(n ^ 2)的常用算法有
[1] 冒泡排序
[2] 插入排序
[3] 選擇排序
時間複雜度爲O(nlogn)的常用算法有:
[4] 堆排序
[5] 歸併排序
[6] 快排
時間複雜度爲O(n)的算法有:
[7] 桶排序
[8] 基數排序
// Bubble Sort
public void bubbleSort(int[] A) {
for (int i = 0; i < A.length - 1; i++) {
boolean flag = true;
for (int j = A.length - 1; j > i; j--) {
if (A[j] < A[j - 1]) {
int tmp = A[j - 1];
A[j - 1] = A[j];
A[j] = tmp;
flag = false;
}
}
if (flag) {
break;
}
}
}
// Selection Sort
public void selectSort(int[] A) {
for (int i = 0; i < A.length - 1; i++) {
int smallIndex = i;
for (int j = i + 1; j < A.length; j++) {
if (A[j] < A[smallIndex]) {
smallIndex = j;
}
}
if (smallIndex != i) {
int tmp = A[i];
A[i] = A[smallIndex];
A[smallIndex] = tmp;
}
}
}
// Insertion Sort
public void insertSort(int[] A) {
for (int i = 1; i < A.length; i++) {
if (A[i - 1] > A[i]) {
int tmp = A[i];
int j = i;
while (j > 0 && A[j - 1] > tmp) {
A[j] = A[j - 1];
j--;
}
A[j] = tmp;
}
}
}
public class Solution {
/**
* @param A an integer array
* @return void
*/
private void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
private void merge(int[] arr, int left, int mid, int right) {
int[] tmpArray = new int[arr.length];
int tmpIndex = left;
int leftIndex = left, rightIndex = mid + 1;
while (leftIndex <= mid && rightIndex <= right) {
if (arr[leftIndex] <= arr[rightIndex]) {
tmpArray[tmpIndex++] = arr[leftIndex++];
} else {
tmpArray[tmpIndex++] = arr[rightIndex++];
}
}
while (leftIndex <= mid) {
tmpArray[tmpIndex++] = arr[leftIndex++];
}
while (rightIndex <= right) {
tmpArray[tmpIndex++] = arr[rightIndex++];
}
for (int i = left; i <= right; i++) {
arr[i] = tmpArray[i];
}
}
private void quickSort(int[] arr, int left, int right) {
if (left < right) {
int pos = partition(arr, left, right);
quickSort(arr, left, pos - 1);
quickSort(arr, pos + 1, right);
}
}
private int partition(int[] arr, int left, int right) {
int key = arr[left];
while (left < right) {
while (left < right && arr[right] >= key) {
right--;
}
if (left < right) {
arr[left++] = arr[right];
}
while (left < right && arr[left] <= key) {
left++;
}
if (left < right) {
arr[right--] = arr[left];
}
}
arr[left] = key;
return left;
}
public void sortIntegers2(int[] A) {
quickSort(A, 0, A.length - 1);
// mergeSort(A, 0, A.length - 1);
}
}
461. Kth Smallest Numbers in Unsorted Array
給定一個無序數組,求第K個最小值是多少。這道題的特點是求第K小,而不是求前K個最小的。如果是求前K個最小的數,那顯然是用堆排序來做了。那既然只是求第K個最小的元素,我們是有辦法做到線性時間複雜度的:那就是利用快排的思想。因爲快排的partition函數,每次partition完後,key左邊的元素都比它小,key右邊的元素都比它大,假定key的下表是K,那麼key就是第(K + 1)小的數了。而這個思想可以結合二分查找來解決求第K小的數:
class Solution {
/*
* @param k an integer
* @param nums an integer array
* @return kth smallest element
*/
int quickSelect(int[] arr, int left, int right, int k) {
int pos = partition(arr, left, right);
if (pos == k - 1) {
return arr[pos];
} else if (pos < k - 1) {
return quickSelect(arr, pos + 1, right, k);
} else {
return quickSelect(arr, left, pos - 1, k);
}
}
public int partition(int[] arr, int left, int right) {
int key = arr[left];
while (left < right) {
while (left < right && arr[right] >= key) {
right--;
}
if (left < right) {
arr[left++] = arr[right];
}
while (left < right && arr[left] <= key) {
left++;
}
if (left < right) {
arr[right--] = arr[left];
}
}
arr[left] = key;
return left;
}
public int kthSmallest(int k, int[] nums) {
return quickSelect(nums, 0, nums.length - 1, k);
}
};
求第K大的元素,思想和上題類似,不再贅述。
class Solution {
/*
* @param k : description of k
* @param nums : array of nums
* @return: description of return
*/
// public int kthLargestElement(int k, int[] nums) {
// Queue<Integer> q = new PriorityQueue<Integer>();
// for (int i = 0; i < nums.length; i++) {
// if (q.size() < k) {
// q.offer(nums[i]);
// } else {
// if (nums[i] > q.peek()) {
// q.poll();
// q.offer(nums[i]);
// }
// }
// }
// return q.poll();
// }
public int kthLargestElement(int k, int[] nums) {
if (nums == null || nums.length == 0 || k <= 0) {
return 0;
}
return quickSelect(nums, 0, nums.length - 1, k);
}
private int quickSelect(int[] arr, int left, int right, int k) {
int pos = partition(arr, left, right);
if (pos == k - 1) {
return arr[pos];
} else if (pos < k - 1) {
return quickSelect(arr, pos + 1, right, k);
} else {
return quickSelect(arr, left, pos - 1, k);
}
}
private int partition(int[] arr, int left, int right) {
int key = arr[left];
while (left < right) {
while (left < right && arr[right] <= key) {
right--;
}
if (left < right) {
arr[left++] = arr[right];
}
while (left < right && arr[left] >= key) {
left++;
}
if (left < right) {
arr[right--] = arr[left];
}
}
arr[left] = key;
return left;
}
};
給定一個數組,要求對這些數字進行組合,使得組合成的整數最大。
我們以輸入兩個數9, 97爲例,將這兩個數組成一個大數後可以得到997,979,此時很容易比較這兩個數,得到較大的數是997,那麼這個數和原來的兩個數有什麼關係呢。 如果我們之間將9, 97排序,然後將較大的數放在前,較小的數在後,得到979顯然不對。
可行的方法是將兩個數都做爲字符串,然後進行字符串拼接,這時就可以得到兩個字符串形式的997,979。然後用字符串比較,此時的比較結果與數字的比較結果是相同的。 將上述兩個數的比較思路推廣到整個數組就可以得到整個數組的排序方法,然後將排序後的數字拼到一起,就是我們要求的結果。
注意:當最大的數字是0時,說明此時輸入的數組中僅包括1個或多個0,此時直接返回結果0,若此時繼續做字符串拼接,可能返回00這樣的結果,是錯誤的。
public String largestNumber(int[] num) {
// 先轉換爲String數組
String[] str = new String[num.length];
for (int i = 0; i < num.length; i++) {
str[i] = "" + num[i];
}
// 按照指定規則排序
Comparator comp = new Comparator<String>() {
public int compare(String s1, String s2) {
return (s2+s1).compareTo(s1+s2);
}
};
Arrays.sort(str, comp);
// 處理000···的情況
if (str[0].equals("0")) {
return "0";
}
// 把String數組轉換爲一個String
String res = "";
for (String s : str) {
res += s;
}
return res;
}
需要注意一下Comparator接口以及compareTo函數的用法。
a.compareTo(b)比較的是a和b的大小,比如'a'.compareTo('c')得到的結果是-2。因爲a在c前面兩個位置。而“979”.compareTo('997')得到的結果是-18,因爲979比997小18。
對於compare(a, b)來說,如果返回的結果是負數,則把a排到b後面,即而返回的是一個正數的話,則把a排到b前邊。
假設s1爲9,s2爲97。則(s2+s1).compareTo(s1+s2)是大於0的,即正數。而返回正數,就把s1排到s2前面,即9排到97前面,這樣就是正確的。
在無序的二維數組中找到第K大的元素,非常典型的解法就是用Heap堆排序,維護一個大小爲K的Heap(即優先隊列),
算法思想:當Heap的大小還沒到K時,則繼續往裏面offer添加數字。如果大小超過K了,則把當前外來的數字和Heap裏最小的元素替換一下(若外來數字比裏面最小的數字大,則Pop出Heap裏最小的數字,然後加入新的外來數字)。如此往復直到遍歷完整個二維數組。這樣可以保證當Heap裏存的K個元素都是最大的(因爲小的都被替換出去了)。
時空複雜度分析:插入K個元素,每次插入的時間複雜度是O(logK),插入N次的時候時間複雜度是O(NlogK),最後再把最小元素pop出去,pop的時間複雜度是O(1)。空間複雜度是O(K)。
public int KthInArrays(int[][] arrays, int k) {
if (arrays == null || arrays.length == 0) {
return 0;
}
Queue<Integer> queue = new PriorityQueue<Integer>();
for (int[] array : arrays) {
for (int element : array) {
if (queue.size() < k) {
queue.offer(element);
} else {
if (element > queue.peek()) {
queue.poll();
queue.offer(element);
}
}
}
}
return queue.poll();
}
401. Kth Smallest Number in Sorted Matrix
在一個行列有序的矩陣中找到第K小的元素。每一行都是有序的,每一列都是有序的。其實也可以用Heap來處理。
其實從結構上來看,可以把這個矩陣看成一棵樹(對角線爲深度),root就是第一個元素。然後可以用BFS的思想來處理這個。每次都pop出一個元素,然後把那個元素的右邊元素和下面元素都加入heap,循環進行K-1次這個操作,就可以把最小的K-1個元素都pop出去。最後再pop一下,就得到了第K小的元素了。需要注意的是每個元素訪問後要標記爲已訪問,以避免重複訪問。這便是二叉樹和矩陣的區別了,因爲二叉樹是不會重複訪問的,所以不用標記已訪問,但是矩陣是連起來的,所以要避免重複訪問。
class Number {
int x, y;
int value;
public Number(int x, int y, int value) {
this.x = x;
this.y = y;
this.value = value;
}
}
class NumComparator implements Comparator<Number> {
public int compare(Number a, Number b) {
return a.value - b.value;
}
}
public class Solution {
/**
* @param matrix: a matrix of integers
* @param k: an integer
* @return: the kth smallest number in the matrix
*/
public boolean valid(int x, int y, int[][] m, boolean[][] visit) {
if (x < m.length && y < m[x].length && !visit[x][y]) {
return true;
}
return false;
}
public int kthSmallest(int[][] matrix, int k) {
if (matrix == null || matrix.length == 0) {
return 0;
}
boolean[][] visit = new boolean[matrix.length][matrix[0].length];
Queue<Number> queue = new PriorityQueue<Number>(k, new NumComparator());
queue.offer(new Number(0, 0, matrix[0][0]));
for (int i = 0; i < k - 1; i++) {
Number currentSmallest = queue.poll();
int x = currentSmallest.x;
int y = currentSmallest.y;
if (valid(x + 1, y, matrix, visit)) {
queue.offer(new Number(x + 1, y, matrix[x + 1][y]));
visit[x + 1][y] = true;
}
if (valid(x, y + 1, matrix, visit)) {
queue.offer(new Number(x, y + 1, matrix[x][y + 1]));
visit[x][y + 1] = true;
}
}
return queue.poll().value;
}
}
465. Kth Smallest Sum In Two Sorted Arrays
有2個有序的數組,從第一個數組中去一個數字,第二個數組中去一個數字,相加的和。要求第K個最小的和是多少。跟上道題類似,也可以用堆來做。
class Number {
int x, y, sum;
public Number(int x, int y, int sum) {
this.x = x;
this.y = y;
this.sum = sum;
}
}
class NumComparator implements Comparator<Number> {
public int compare(Number a, Number b) {
return a.sum - b.sum;
}
}
public class Solution {
/**
* @param A an integer arrays sorted in ascending order
* @param B an integer arrays sorted in ascending order
* @param k an integer
* @return an integer
*/
public boolean valid(int x, int y, int[] A, int[] B, boolean[][] visit) {
if (x < A.length && y < B.length && !visit[x][y]) {
return true;
}
return false;
}
public int kthSmallestSum(int[] A, int[] B, int k) {
int m = A.length;
int n = B.length;
boolean[][] visit = new boolean[m][n];
Queue<Number> queue = new PriorityQueue<Number>(k, new NumComparator());
queue.offer(new Number(0, 0, A[0] + B[0]));
for (int i = 0; i < k - 1; i++) {
Number head = queue.poll();
int x = head.x;
int y = head.y;
if (valid(x + 1, y, A, B, visit)) {
queue.offer(new Number(x + 1, y, A[x + 1] + B[y]));
visit[x + 1][y] = true;
}
if (valid(x, y + 1, A, B, visit)) {
queue.offer(new Number(x, y + 1, A[x] + B[y + 1]));
visit[x][y + 1] = true;
}
}
return queue.poll().sum;
}
}
這道題讓我們求擺動排序,跟Wiggle Sort II相比起來,這道題的條件寬鬆很多,只因爲多了一個等號。由於等號的存在,當數組中有重複數字存在的情況時,也很容易滿足題目的要求。這道題我們首先會想到一種時間複雜度爲O(nlgn)的方法,思路是先給數組排個序,然後我們只要每次把第三個數和第二個數調換個位置,第五個數和第四個數調換個位置,以此類推直至數組末尾,這樣我們就能完成擺動排序了。但是問題是這個算法會超時,我們需要一個更快的解法。
這道題還有一種O(n)的解法,根據題目要求的nums[0] <= nums[1] >= nums[2] <= nums[3]....,我們可以總結出如下規律:
當i爲奇數時,nums[i] >= nums[i - 1]
當i爲偶數時,nums[i] <= nums[i - 1]
那麼我們只要對每個數字,根據其奇偶性,跟其對應的條件比較,如果不符合就和前面的數交換位置即可,參見代碼如下:
public void swap(int[] nums, int a, int b) {
int tmp = nums[a];
nums[a] = nums[b];
nums[b] = tmp;
}
public void wiggleSort(int[] nums) {
int n = nums.length;
for (int i = 1; i < n; i++) {
if (i % 2 == 1 && nums[i] < nums[i-1]) {
swap(nums, i, i-1);
}
if (i % 2 == 0 && nums[i] > nums[i-1]) {
swap(nums, i, i-1);
}
}
}
這道題與上道題的不同之處在於把等號去掉了:nums[0] < nums[1] > nums[2] < nums[3]....
所以上面的解法行不通了,因爲這種例子:Given nums = [1, 5, 1, 1, 6, 4], one possible answer is [1, 4, 1, 5, 1, 6].
我們可以先給數組排序,然後在做調整。調整的方法是找到數組的中間的數,相當於把有序數組從中間分成兩部分,然後從前半段的末尾取一個,在從後半的末尾去一個,這樣保證了第一個數小於第二個數,然後從前半段取倒數第二個,從後半段取倒數第二個,這保證了第二個數大於第三個數,且第三個數小於第四個數,以此類推直至都取完。
public void wiggleSort(int[] nums) {
int n = nums.length;
int[] tmp = new int[n];
for (int i = 0; i < n; i++) {
tmp[i] = nums[i];
}
Arrays.sort(tmp);
int left = (n+1)/2, right = n;
for (int i = 0; i < n; i++) {
nums[i] = (i % 2 == 0) ? tmp[--left] : tmp[--right];
}
}
那有沒有更快的方法呢?有的,可以用快速選擇法,我們只要找到中位數即可,用快排把數組排到這種程度:第n/2個元素左邊的元素都比它小,右邊的元素都比它大。然後從前半段的末尾取一個,在從後半的末尾去一個,這樣保證了第一個數小於第二個數,然後從前半段取倒數第二個,從後半段取倒數第二個,這保證了第二個數大於第三個數,且第三個數小於第四個數,以此類推直至都取完。