分治思想是常见的算法思想之一,在排序算法中用到分治思想的有:快速排序和归并排序。
分治法介绍如下图:
分治思想的关键点:
1、有问题可以一直分解为形式相同的子问题,当子问题规模较小时,可自然求解,例如一个元素本身有序。
2、子问题的解通过合并可以得到有问题的解。
3、子问题的分解以及解的合并一定是比较简单的,否则分解和合并所花的时间可能超出暴力解法,得不偿失。
一、快速排序
1、一遍单向扫描法(只用两个指针)
代码如下:
import java.util.*;
public class 快速排序 {
public static void main(String[] args){
int[] a = new int[]{2,3,4,1,6,8,5,9,};
System.out.println(Arrays.toString(a));
quicksort(a, 0, 7);
System.out.println(Arrays.toString(a));
}
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);
}
}
static int partition(int[] a, int p, int r){
int pi = a[p];
int i = p+1;
int j = r;
while (i<=j){//注意要包含相等的情况
if(a[i]<=pi) i++;
else{
int temp = a[i];
a[i] = a[j];
a[j] = temp;
j--;
}
}
int temp = a[p]; //插入分界点
a[p] = a[j];
a[j] = temp;
return j;
}
}
2、双向扫描法:
双向扫描思路:头尾指针都向中间扫描,从左找到大于主元素的元素,从右找到小于等于主元素的元素二者交换,继续扫描,直到左侧无大元素,右侧无小元素。
最终左右指针会交错,左指针会指向大于主元素的,右指针会指向小于等于主元素的。所以最终应该把右指针的指向和主元素进行交换。
代码如下:
import java.util.*;
public class 快速排序之双向扫描 {
public static void main(String[] args){
int[] a = new int[]{2,3,4,1,6,8,5,9,};
quicksort(a, 0, 7);
System.out.println(Arrays.toString(a));
}
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);
}
}
static int partition(int[] a, int p, int r){
int flag = a[p];
int i = p + 1;
int j = r;
while(i<j){
while (a[i]<=flag){
i++;
}
while (a[j]>flag){
j--;
}
if(i<j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
int temp = a[p]; //注意是和右指针j交换
a[p] = a[j]; //因为右指针的指向一定是小于等于主元素的
a[j] = temp; //因为主元素取得是第一个元素,需要是小的
return p;
}
}
二、归并排序
步骤如下:
分解:将n个元素分成各含有n/2个元素的子序列;
解决:对两个子序列递归排序
合并:合并两个已排序的子序列以得到排序结果
与快速排序的不同点:归并的分解比较随意,重点是合并。
import java.util.Arrays;
public class 归并排序 {
public static void main(String[] args){
int[] a = new int[]{2,3,4,1,6,8,5,9,};
System.out.println(Arrays.toString(a));
mergesort(a, 0, a.length-1);
System.out.println(Arrays.toString(a));
}
static void mergesort(int[] a, int p, int r){
if(p<r){
int mid = (p + r)/2;
mergesort(a, p, mid);
mergesort(a, mid+1, r);
merge(a, p, mid, r);
}
}
static void merge(int[] a, int p, int mid, int r){
int left = p;
int right = mid + 1;
int current = p;
int[] helper = new int[a.length];//辅助数组
System.arraycopy(a, 0, helper, 0, a.length);
while (left<=mid && right<=r){
if(helper[left]<helper[right]){
a[current] = helper[left];
current++;
left++;
}
else{
a[current] = helper[right];
current++;
right++;
}
}
while(left<=mid){//把左边剩余部分补充进去,
a[current] = helper[left];
current++;
left++;
}
while(right<=r){//把左边剩余部分补充进去
a[current] = helper[right];
current++;
right++;
}
}
}
快速排序思想的运用:
一、奇数在左偶数在右
问题描述:输入一个整数数组,调整数组中数组的顺序,使得所有奇数位于数组的前半部,所有偶数位于数组的后半部。要求时间复杂度为O(n)。
解题思路:用快速排序的思想,分别用两个指针指向数组的头和尾,两个指针向中间移动,左边找到一个偶数则停止,右边找到一个奇数则停止;然后交换两个数。接着移动,直到两个指针交错。
代码如下:
import java.util.Arrays;
public class 奇数在左偶数在右 {
public static void main(String[] args){
int[] a = new int[]{2,3,4,1,6,8,5,9,};
System.out.println(Arrays.toString(a));
f(a, 0, a.length-1);
System.out.println(Arrays.toString(a));
}
static void f(int[] a, int p, int r){
int left = p;
int right = r;
while(left<right){
while(a[left]%2==1 && left<a.length){//找奇数
left++;
}
while(a[right]%2==0 && right>0){//找偶数
right--;
}
if(left<=right){
int temp = a[left];
a[left] = a[right];
a[right] = temp;
}
}
}
}
二、以尽量最快的效率求出乱序数组中第k小的数
例如:数组{3,9,7,6,1,2}中第二小的数是2;第一小的数是1。
解题思路:是将快速排序和二分查找的思想结合起来。一趟快速排序后返回的中间数的下标为b,那么中间数就是第b+1小的数。
通过比较就可以确定接下来在左边查找还是右边查找。
代码如下:
import java.util.Scanner;
public class 第k小的数 {
public static void main(String[] args){
int[] a = new int[]{2,3,4,1,6,8,5,9,};
Scanner scanner = new Scanner(System.in);
int k = scanner.nextInt();
System.out.println(selectK(a, 0, a.length-1, k));
}
static int selectK(int[] a, int p, int r, int k){
int q = partion(a, p, r);
int qk = q +1;
if(qk==k) return a[q];
else if(qk<k) return selectK(a, q+1, r, k);
else return selectK(a, p, q-1, k);
}
static int partion(int[] a, int p, int r) {
int n = a[p];
int left = p+1;
int right = r;
while(left<=right){
while(a[left]<=n && left<a.length ) //注意要有等号,否则数组中有重复数字时会出现死循环
left++;
while(a[right]>=n && right>0) //注意要有等号,否则数组中有重复数字时会出现死循环
right--;
if(left<=right){
int temp = a[left];
a[left] = a[right];
a[right] = temp;
}
}
int temp = a[right];
a[right] = a[p];
a[p] = temp;
return right;
}
}
三、 出现次数超过数组长度的一半
问题描述:数组中有一个数字出现的次数超过了数组的长度的一半,找出这个数字。
解题思路:这一题相当于是上一题的变形。如果数组排好序后,第n/2个元素一定是那个出现次数超过数组长度一半的数。所以这题就变成找第n/2小的元素。当然这题也有其他解法,例如排序后找第n/2个元素,或者用哈希统计。
代码如下:
import java.util.*;
public class 出现的次数超过数组长度的一半 {
public static void main(String[] args){
int[] a = new int[]{8,8,8,1,6,8,8,9,};
Scanner scanner = new Scanner(System.in);
//int k = scanner.nextInt();
System.out.println(selectK(a, 0, a.length-1, (a.length-1)/2));
}
static int selectK(int[] a, int p, int r, int k){
int q = partion(a, p, r);
int qk = q +1;
if(qk==k) return a[q];
else if(qk<k) return selectK(a, q+1, r, k);
else return selectK(a, p, q-1, k);
}
static int partion(int[] a, int p, int r) {
int n = a[p];
int left = p+1;
int right = r;
while(left<=right){
while(a[left]<=n && left<a.length ) //注意要有等号,否则数组中有重复数字时会出现死循环
left++;
while(a[right]>=n && right>0) //注意要有等号,否则数组中有重复数字时会出现死循环
right--;
if(left<=right){
int temp = a[left];
a[left] = a[right];
a[right] = temp;
}
}
int temp = a[right];
a[right] = a[p];
a[p] = temp;
return right;
}
}
四、最小可用ID
问题描述:在非负数组(乱序无重复)中找到最小的可分配的ID(从1开始编号),数据量1000000
解题思路:这题和找第K小的数有相似的地方,也是将快速排序和二分查找的思想结合起来。一趟快速排序后返回的中间数的下标为i,中间数为num,如果这时num =i +1, 那么说明最小可分配的ID不是出现在中间数的左侧,如果num< i+1, 那么说明最小可分配ID是出现在中间数的左侧。通过判断可以缩小范围进行下一轮的递归。
代码省略。