运用分治思想的排序(快速排序和归并排序)



分治思想是常见的算法思想之一,在排序算法中用到分治思想的有:快速排序和归并排序。
分治法介绍如下图:
在这里插入图片描述
分治思想的关键点:
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是出现在中间数的左侧。通过判断可以缩小范围进行下一轮的递归。
代码省略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逍遥自在”

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值