排序作为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个元素左边的元素都比它小,右边的元素都比它大。然后从前半段的末尾取一个,在从后半的末尾去一个,这样保证了第一个数小于第二个数,然后从前半段取倒数第二个,从后半段取倒数第二个,这保证了第二个数大于第三个数,且第三个数小于第四个数,以此类推直至都取完。