排序
插入排序
插入排序适合元素较少的情况。
循环不变式
- 初始化:循环的第一次迭代之前,它为真。
- 保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。
- 终止:在循环终止时,不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的。
代码实现
-
升序
public static void insertionSort(int[] A) { for (int i = 1; i < A.length; i++) { int key = A[i]; int j = i - 1; while (j >= 0 && A[j] > key) { A[j + 1] = A[j]; j--; } A[j + 1] = key; } }
-
降序
public static void insertionSort(int[] A) { for (int i = 1; i < A.length; i++) { int key = A[i]; int j = i - 1; while (j >= 0 && A[j] < key) { A[j + 1] = A[j]; j--; } A[j + 1] = key; } }
最好的情况是数组本来就完全有序或基本有序,时间复杂度可以降到O(n),平均情况下,时间复杂度为O(n^2^),空间复杂度为O(1).
注意:将插入过程中的查找过程使用二分查找可以有效地减少比较次数,但移动元素的次数仍然不变,此时运行总时间依然是O(n^2^),而不会变成O(nlgn)。
算法习题
把两个n位二进制数相加。
public static int[] addBinary(int[] A,int[] B) {
int[] C = new int[A.length + 1];
int carry = 0;
for (int i = 0; i < A.length; i++) {
C[i] = (A[i] + B[i] + carry) % 2;
carry = (A[i] + B[i] + carry) / 2;
}
C[i] = carry;
return C;
}
归并排序
归并排序算法完全遵循分治模式。
-
分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列
-
解决:使用归并排序递归地排序两个子序列
-
合并:合并两个已排序的子序列以产生已排序的答案
代码实现
public static void mergeSort(int[] A,int p,int r) {
if (p < r) {
int q = (p + r) / 2;
mergeSort(A,p,q);
mergeSort(A,q + 1,r);
merge(A,p,q,r);
}
}
public static void merge(int[] A,int p,int q,int r) {
int n1 = q - p + 1;
int n2 = r - q;
int[] L = new int[n1 + 1];
int[] R = new int[n2 + 1];
for (int i = 0; i < n1; i++) {
L[i] = A[p + i];
}
for (int j = 0; j < n2; j++) {
R[j] = A[q + j + 1];
}
//设置两个哨兵
L[n1] = Integer.MAX_VALUE;
R[n2] = Integer.MAX_VALUE;
int i = 0,j = 0;
for (int k = p; k <= r; k++) {
if (L[i] <= R[j]) {
A[k] = L[i];
++i;
} else {
A[k] = R[j];
++j;
}
}
}
如果merge中不使用哨兵,则代码应修改为:
public static void merge(int[] A,int p,int q,int r) {
int n1 = q - p + 1;
int n2 = r - q;
int[] L = new int[n1];
int[] R = new int[n2];
for (int i = 0; i < n1; i++) {
L[i] = A[p + i];
}
for (int j = 0; j < n2; j++) {
R[j] = A[q + j + 1];
}
int i = 0,j = 0;
int k;
for (k = p; i < L.length && j < R.length; k++) {
if (L[i] <= R[j]) {
A[k] = L[i];
++i;
} else {
A[k] = R[j];
++j;
}
}
//下面两个循环只会执行其中一个
while (i < L.length) {
A[k] = L[i];
++i;
++k;
}
while (j < R.length) {
A[k] = R[j];
++j;
++k;
}
}
运行时间分析
分解:计算数组中间位置,需要常量是时间。
解决:递归地求解两个规模均为n/2的子问题,将贡献2T(n/2)的运行时间
合并:merge需要Θ(n)的时间
递归式为:
由主定理可得递归式的解为:
算法习题
确定n个整数的集合S中是否存在两个数其和刚好为x(S,x为参数)要求时间复杂度为O(nlgn)。
public static boolean practise(int[] A,int x) {
mergeSort(A,0,A.length - 1);
for (int i = 0; i < A.length; i++) {
if (binarySearch(A,0,A.length - 1,i,x - A[i])) {
return true;
}
}
return false;
}
//归并排序,时间复杂度为O(nlgn)
public static void mergeSort(int[] A,int p,int r) {
if (p < r) {
int q = (p + r) / 2;
mergeSort(A,p,q);
mergeSort(A,q + 1,r);
merge(A,p,q,r);
}
}
public static void merge(int[] A,int p,int q,int r) {
int n1 = q - p + 1;
int n2 = r - q;
int[] L = new int[n1 + 1];
int[] R = new int[n2 + 1];
for (int i = 0; i < n1; i++) {
L[i] = A[p + i];
}
for (int j = 0; j < n2; j++) {
R[j] = A[q + j + 1];
}
L[n1] = Integer.MAX_VALUE;
R[n2] = Integer.MAX_VALUE;
int i = 0,j = 0;
for (int k = p; k <= r; k++) {
if (L[i] <= R[j]) {
A[k] = L[i];
++i;
} else {
A[k] = R[j];
++j;
}
}
}
//二分查找,时间复杂度为O(lgn)
public static boolean binarySearch(int[] A,int low,int high,int index,int x) { //传入index是为了保证找到的不是已经减去的值(如8 - 4 = 4)
while (low <= high) {
int mid = (low + high) / 2;
if (x == A[mid] && mid != index) {
return true;
} else if (x < A[mid]) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return false;
}
快速排序
主要思想:与归并排序一样。快速排序也使用了分治的思想。
过程和实现
- 分解:数组A[p..r]被划分为两个子数组A[p...q - 1]和A[q + 1,..r],其中A[p...q - 1]小于等于A[q],A[q + 1,..r]大于等于A[q].
- 解决:通过递归调用快速排序,对子数组A[p...q - 1]和A[q + 1,..r]进行排序。
- 合并:因为子数组都是原址排序的,不需要进行合并操作:数组A[p..r]已经有序。
代码实现(java):
public static void quickSort(int[] A,int p,int r) {
if (p < r) {
int q = partition(A,p,r);
quickSort(A,p,q - 1);
quickSort(A,q + 1,r);
}
}
public static int partition(int[] A,int p,int r) {
int x = A[r];
int i = p - 1;
for (int j = p; j <= r - 1; j++) {
if (A[j] <= x) { //降序则只需将此处修改为if(A[j] >= x)
i = i + 1;
swap(A,i,j);
}
}
swap(A,i + 1,r);
return i + 1;
}
public static void swap(int[] A,int i,int j) {
int x = A[i];
A[i] = A[j];
A[j] = x;
}
快速排序的性能
-
最坏情况划分
当划分产生的两个子问题分别包含了n - 1个元素和0个元素时,快速排序的最坏情况发生。划分操作的时间复杂度为Θ(n²),算法运行时间的递归式为
利用代入法可直接得到其解为
所以当输入数组完全有序时,快速排序的时间复杂度是Θ(n²),而在同样情况下插入排序的时间复杂度O(n).。
-
最好情况划分
当partition得到的两个子问题的规模都不大于n/2时,快速排序的性能最好。算法运行时间的递归式为:
利用主方法可得到其解为
快速排序的平均情况更接近于其最好的情况,而非最坏的情况。
特殊情况:
-
情况一:
当数组中的元素都具有相同值时,QuickSort的时间复杂度是Θ(n^2^)。因为每次partition都会返回r,将数组划分为n - 1和0的子数组。为了让其此情况下可以返回(p + r) / 2,可作如下修改:
public static int partition(int[] A,int p,int r) {
int x = A[r];
int i = p - 1;
for (int j = p; j <= r - 1; j++) {
if (A[j] <= x) {
i = i + 1;
swap(A,i,j);
}
}
swap(A,i + 1,r);
if (i + 1 == r) {
return (p + r) / 2;
} else {
return i + 1;
}
}
-
情况二:
当数组中的元素是降序排列时,QuickSort的时间复杂度为Θ(n^2^)。因为每次partition都会返回p(因为都比枢纽值大),将数组划分为0和n - 1的子数组。
-
情况三:
当数组中的元素基本满足升序排列时,QuickSort的时间复杂度为Θ(n^2^)。因为每次partition都会返回r(因为都比枢纽值小),将数组划分为你- 1和n0的子数组。
快速排序的随机化版本
前面都是选择数组的最后一个元素作为主元,现在为了随机化,可以从A[p..r]中随机选择一个元素作为主元,此时主元素是等概率地从数组中选取的。
代码实现:
public static void quickSort(int[] A,int p,int r) {
if (p < r) {
int q = randomizedPartition(A,p,r);
quickSort(A,p,q - 1);
quickSort(A,q + 1,r);
}
}
public static int randomizedPartition(int[] A,int p,int r) {
Random random = new Random();
int i = random.nextInt(r - p + 1) + p;
swap(A,i,r);
return partition(A,p,r);
}
public static int partition(int[] A,int p,int r) {
int x = A[r];
int i = p - 1;
for (int j = p; j <= r - 1; j++) {
if (A[j] <= x) {
i = i + 1;
swap(A,i,j);
}
}
swap(A,i + 1,r);
return i + 1;
}
public static void swap(int[] A,int i,int j) {
int x = A[i];
A[i] = A[j];
A[j] = x;
}
快速排序的运行时间是由在partitition操作上所花费的时间决定的。每调用一次partition都会选择一个主元素,而且该元素不会被包含在后续的quicksort和partition调用中。因此最多会调用n次partition操作。
整个过程中,数组中的每对元素最多比较一次,最少0次。
比较一次:因为每个元素至于主元素进行比较,在某一次划分操作后,此次调用所用的主元素就再也不会对任何其他元素进行比较了。
比较零次:被划分成两部分的元素永远不会进行比较。
计数排序 基本思想 对每一个输入元素想,确定小于x的元素个数。利用这一信息直接把x放到它在输出数组中的位置上。
代码实现
public static void countingSort(int[] A,int[] B,int k) {
int[] C = new int[k + 1];
for (int i = 0; i < A.length; i++) {
C[A[i]]++;
}
for (int i = 1; i < k + 1; i++) {
C[i] += C[i - 1]; //每个元素表示的是数组中每个数自己加上小于等于它的数的个数
}
for (int i = A.length - 1; i >= 0; i--) {
B[C[A[i]] - 1] = A[i];
C[A[i]]--;
}
}
public static int findMaxValue(int[] A) { //用来产生k
int max = Integer.MIN_VALUE;
for (int i = 0; i < A.length; i++) {
if (A[i] > max) {
max = A[i];
}
}
return max;
}
时间复杂度为O(n + k),空间复杂度为O(n + k)
计数排序不需要进行比较,而且是稳定的,这种稳定性只有当进行排序的数据还附带卫星数据时才比较重要。计数排序经常会被用作基数排序算法的一个子过程。
稳定的原因
- 比如两个数值相等元素A[i] = A[j],其中i<j,在计数过程中,位于前面的A[i]总是先被计入,且在后面还原数组时,由于是从后往前遍历放置,原来在前面的元素小于等于它的数要更少,故可以保证其稳定性。
如果最后一个循环不是从后往前,而改为从前往后即i = 0 -->A.length - 1,仍可以完成排序,但无法保证稳定性。
算法拓展
根据该思想我们可以在O(1)时间内求出输入的n个整数落在区间[a,b]的个数。预处理阶段就是计数阶段,其时间复杂度为O(n + k)。
代码实现
public static int[] countNumbers(int[] A,int k) { //预处理
int[] C = new int[k + 1];
for (int i = 0; i < A.length; i++) {
C[A[i]]++;
}
for (int i = 1; i < k + 1; i++) {
C[i] += C[i - 1];
}
return C;
}
public static int countNumbersOfa_b(int[] C,int a,int b) {
return C[b] - C[a - 1];
}