3. 查找、排序(下)
3.1 分治法(快速排序,归并排序)
分治法:将原问题划分成若干个规模较小而结构与原问题一致的子问题;递归的解决这些子问题,然后再合并其结果,就得到原问题的解。
三个步骤:
分解:将原问题划分成若干个规模较小而结构与原问题一致的子问题
解决:递归的解决这些子问题。若问题足够小,则直接有解。
合并:合并其结果
3.1.1 快速排序(重点在于划分)
分解:数组划分成两个子数组,左边的数小于居中的数,右边的数大于居中的数。计算下标q也是划分过程的一部分。
解决:通过递归调用快速排序,对子数组进行排序。
合并:因为子数组都是原址排序的,所以不需要合并,数组已经有序。
一遍单向扫描法:(先定主元)
用两个指针将数组划分为三个区间,
扫描指针左边是确认小于等于主元的,
扫描指针到某个指针中间为未知的,因此我们将第二个指针称为未知区间末指针,末指针的右边区间为默认大于主元素的。
import java.util.Arrays;
public class 快速排序 {
public static void main(String[] args) {
int[] arr = new int[] { 3, 4, 7, 2, 4, 3, 1, 4, 5, 9 };
quickSort(0, arr.length - 1, arr);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int left, int right, int[] arr) {
if (left < right) {
int index = partition(left, right, arr);
// 对左边进行排序
quickSort(left, index, arr);
// 对右边进行排序
quickSort(index + 1, right, arr);
}
}
//将小于arr[left]的值放在左边,将大于arr[left]的值放在右边,并返回新的left的值
private static int partition(int left, int right, int[] arr) {
// TODO Auto-generated method stub
//基准点
int base = arr[left];
//基准点的下表
int indexBase = left;
//当left = right 退出循环
while (left < right) {
//找到需要交换的点
while (base <= arr[right]&&left < right) {
right--;
}
while (base >= arr[left]&&left < right) {
left++;
}
//交换这两个点
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
arr[indexBase] = arr[left];
arr[left] = base;
//base = arr[right];
return left;
}
}
3.1.2 归并排序
84571362
8457 1362 分
84 57 13 62
8 4 5 7 1 3 6 2
==================================================================================================================================================================================================================================================================================================================================
48 57 13 26
4578 1236 治
12345678
public class 归并排序 {
public static void main(String[] args) {
int[] arr = new int[] { 8, 4, 5, 7, 1, 3, 6, 2 };
int[] temp = new int[arr.length];
mergeSort(arr, 0, arr.length-1, temp);
System.out.println(Arrays.toString(arr));
}
/**
* 归并排序 = 拆分 + 处理 + 合并
* @param arr 要排序的数组
* @param left 要排序的数组起始位置下标
* @param right 要排序的数组末尾下标
* @param temp 辅助临时数组
*/
static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
// 中间索引下标
int mid = left + ((right - left) >> 1);
// 向左递归进行分解
mergeSort(arr, left, mid, temp);
// 向右递归进行分解
mergeSort(arr, mid+1, right, temp);
// 合并
merge(arr, left, mid, right, temp);
}
}
/**
* 合并: 治阶段
*
* @param arr
* @param left
* @param mid
* @param right
* @param temp
*/
static void merge(int[] arr, int left, int mid, int right, int[] temp) {
// 左边起始位置指针
int i = left;
// 右边起始位置指针
int j = mid + 1;
// 临时数组起始位置指针
int t = 0;
// 先把左右两边(有序)的数据按照规则填充到temp数组,直到左右两边的有序数列,有一边处理完毕为止
while (i <= mid && j <= right) {
// 如果左边的有序序列的当前元素,小于右边有序序列的当前元素
if (arr[i] < arr[j]) {
// 则将左边的当前元素,填充到temp数组
temp[t] = arr[i];
// 然后t++,i++
t++;
i++;
} else {
// 反之,则将右边有序序列的当前元素,填充到temp数组
temp[t] = arr[j];
// 然后t++,i++
t++;
j++;
}
}
// 把有剩余数据的一边的数据,一次全部填充到temp
// 左边未到头
while (i <= mid) {
temp[t] = arr[i];
t++;
i++;
}
// 右边未到头
while (j <= right) {
temp[t] = arr[j];
t++;
j++;
}
// 将temp数组拷贝回arr数组中
// 临时数组初始位置
t = 0;
// 目标数组最左边的位置(最左边的起始位置不一定是0哦!)
int l = left;
while (l <= right) {
arr[l] = temp[t];
t++;
l++;
}
}
}
3.2 算法案例
3.2.1 调整数组顺序使奇数在偶数前面
题目:输入一个数组,调整数组数字顺序,使奇数在偶数前面。
思路:归并排序思想,开辟一个数组,从头开始扫描,扫描奇数,放到最前,扫描偶数放到最后
import java.util.Arrays;
public class 调整数组顺序使奇数在偶数前面 {
public static void main(String[] args) {
//归并
int[] arr = {1,2,3,4,5,6,7,8};
int[] temp =new int[arr.length];
int left = 0;
int right = arr.length-1;
for (int i = 0; i < arr.length; i++) {
if(arr[i]%2 == 0) {
temp[right] = arr[i];
right--;
}else {
temp[left] = arr[i];
left++;
}
}
System.out.println(Arrays.toString(temp));
}
}
3.2.2 超过一半的数字
题目:数组中有一个数字次数出现了一半以上,请找出
思路:
方法1:排序后,输出中间的那个元素。O(nlgn)
方法2:通过题目3.2.4,求中间那个元素,Select(arr,left,right,arr.length/2); O(n)限制需要改动数组内容
方法3:《寻找发帖水王》通过消除法解决问题,两个不同的就消除,否则计数器增加。
方法4:哈希统计
//方法3:《寻找发帖水王》通过消除法解决问题,两个不同的就消除,否则计数器增加。
static int solve4(int[] arr) {
int candidate=arr[0];//先定第一个元素候选
int num=1;//出现次数
for (int i = 1; i < arr.length; i++) {//从第二个扫描数组
if(num==0){//前面的步骤相消,消为0了,把后面元素作为候选
candidate=arr[i];
num=1;
continue;
}
if(candidate==arr[i]){//遇到和候选值相同的,次数加1
num++;
}else {//不同的数,进行消减
num--;
}
}
return candidate;
}
3.2.3 最小可用id
题目:在非负数组(乱序)中找到最小可分配的id(从1开始编号),数据量1000000
思路:3 2 1 4 5 6 7 8 9 11 12 。。。。。 最小可用id为10
方法1:排序后,选择第一个缺席的元素 O(NlgN)
方法2:暴力,一个一个试
方法3:定义一个辅助数组,从1-1000,然后从数组找,找到一个就从辅助数组设为1,最后找0第一次出现的位置。
// O(N²) 暴力解法:从1开始依次探测每个自然数是否在数组中
static int find1(int[] arr) {
int i = 1;
while (true) {
if (Util.indexOf(arr, i) == -1) {
return i;
}
i++;
}
}
// NlogN
static int find2(int[] arr) {
Arrays.sort(arr);//NlogN
int i = 0;
while (i < arr.length) {
if (i + 1 != arr[i]) { //不在位的最小的自然数
return i + 1;
}
i++;
}
return i + 1;
}
/**
* 改进1:
* 用辅助数组
*
*/
public static int find3(int[] arr) {
int n = arr.length;
int[] helper = new int[n + 1];
for (int i = 0; i < n; i++) {
if (arr[i] < n + 1)
helper[arr[i]] = 1;
}
for (int i = 1; i <= n; i++) {
if (helper[i] == 0) {
return i;
}
}
return n + 1;
}
/**
* 改进2,分区,递归
* 问题可转化为:n个正数的数组A,如果存在小于n的数不在数组中,必然存在大于n的数在数组中, 否则数组排列恰好为1到n
* @param arr
* @param l
* @param r
* @return
*/
public static int find4(int[] arr, int l, int r) {//l->left,r->right
if (l > r)
return l + 1;
int midIndex = l + ((r - l) >> 1);//中间下标
int q = select(arr, l, r, midIndex - l + 1);//实际在中间位置的值
int t = midIndex + 1;//期望值
if (q == t) {//左侧紧密
return find4(arr, midIndex + 1, r);
} else {//左侧稀疏
return find4(arr, l, midIndex - 1);
}
}
3.2.4 第K个元素
题目:以尽量高的效率求出一个乱序数组中按照数值顺序的第K个元素值。
思路:3 9 7 6 1 2 求第二个是 2 .
方法1:先进行快速排序,再查找。O(NlgN)
方法2:基于快速排序,先选择基值3,进行划分,得到2 1 3 6 7 9,然后再从左边的区间开始找,抛弃一半
//伪代码
Select(A,p,r,k){
q = partition(A,p,r)
qk = q-p+1
if(qk == k)
return A[q]
else if(qk>k)
return Select(A,p,q-1,k);
else
return Select(A,q+1,r,k-qk)
}
partitio(A,p,r){
}
//Java代码
public class 第K个元素 {
public static void main(String[] args) {
int[] arr = {3,9,7,6,1,2};
int left = 0;
int right = arr.length-1;
int k = 2;
System.out.println(Select(arr,left,right,k));
}
private static int Select(int[] arr, int left, int right, int k) {
// TODO Auto-generated method stub
if(left < right) {
int index = partition(arr,left,right);
//左边进行排序
if(index == k-1) {
return arr[index];
}else if(index > k-1)
return Select(arr, left, index, k);
//右边进行排序
else
return Select(arr, index+1, right, k);
}
return -1;
}
private static int partition(int[] arr, int left, int right) {
// TODO Auto-generated method stub
int base = arr[left];
int baseIndex = left;
while(left < right) {
while(left<right && base<= arr[right] ) {
right--;
}
while(left<right && base >= arr[left]) {
left++;
}
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
arr[baseIndex] = arr[left];
arr[left] = base;
return left;
}
}
3.3.5 合并有序数组
题目:给定两个排序后的数组A和B,其中A的末端有足够的缓冲空间容纳B,编写一个方法,将B并入A的排序。
思路:和归并排序相似
两个数组A:1 3 5 7 8 9 10 12 和B:2 4 6 8 10。定义三个指针,P1指向数组A的12,P2指向数组B的10,current指向A的最后一个位置,然后指针P1和P2作比较,大的放到current指针的位置,然后指针向左移动。
import java.util.Arrays;
public class 合并有序数组 {
public static void main(String[] args) {
int[] a = {2,0};
int m = 1;
int[] b = {1};
int n = 1;
merge(a,m,b,n);
}
public static void merge(int[] A, int m, int[] B, int n) {
int p1 = m-1;
int p2 = n-1;
int c = n+m-1;
while(p1>=0&&p2>=0) {
if (A[p1] <= B[p2]) {
A[c] = B[p2];
p2--;
}else {
A[c] = A[p1];
p1--;
}
c--;
}
while(p2>=0) {
A[c] = B[p2];
p2--;
c--;
}
System.out.println(Arrays.toString(A));
}
}
3.3.6 逆序对个数
题目:一个数列,如果左边的数大,右边的数小,则称这两个数位一个逆序对。求出一个数列中有多少个逆序对。
思路:归并排序。
class 逆序对个数{
public static void main(Strings[] args){
int[] nums = {7,5,6,8};
System.out.println(reversePairs(nums));
}
public int reversePairs(int[] nums) {
if(nums.length<2){
return 0;
}
int[] temp = new int[nums.length];
mergeSort(nums, 0, nums.length-1, temp);
return count;
}
/**
* 归并排序 = 拆分 + 处理 + 合并
*
* @param arr 要排序的数组
* @param left 要排序的数组起始位置下标
* @param right 要排序的数组末尾下标
* @param temp 辅助临时数组
*/
void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
// 中间索引下标
int mid = left + ((right - left) >> 1);
// 向左递归进行分解
mergeSort(arr, left, mid, temp);
// 向右递归进行分解
mergeSort(arr, mid + 1, right, temp);
// 合并
merge(arr, left, mid, right, temp);
}
}
/**
* 逆序对个数
*/
static int count = 0;
/**
* 合并: 治阶段
*
* @param arr
* @param left
* @param mid
* @param right
* @param temp
*/
void merge(int[] arr, int left, int mid, int right, int[] temp) {
// 左边起始位置指针
int i = left;
// 右边起始位置指针
int j = mid + 1;
// 临时数组起始位置指针
int t = 0;
// 先把左右两边(有序)的数据按照规则填充到temp数组,直到左右两边的有序数列,有一边处理完毕为止
while (i <= mid && j <= right) {
// 如果左边的有序序列的当前元素,小于右边有序序列的当前元素
if (arr[i] <= arr[j]) {
// 则将左边的当前元素,填充到temp数组
temp[t] = arr[i];
// 然后t++,i++
t++;
i++;
} else {
// 反之,则将右边有序序列的当前元素,填充到temp数组
temp[t] = arr[j];
// 然后t++,i++
t++;
j++;
// 逆序对个数累加
count += mid - i + 1;
}
}
// 把有剩余数据的一边的数据,一次全部填充到temp
// 左边未到头
while (i <= mid) {
temp[t] = arr[i];
t++;
i++;
}
// 右边未到头
while (j <= right) {
temp[t] = arr[j];
t++;
j++;
}
// 将temp数组拷贝回arr数组中
// 临时数组初始位置
t = 0;
// 目标数组最左边的位置(最左边的起始位置不一定是0哦!)
int l = left;
while (l <= right) {
arr[l] = temp[t];
t++;
l++;
}
}
}