蓝桥杯算法课《算法最美》笔记——3. 递归、查找、排序(下)

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 classK个元素 {
	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++;
		}
	}
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大果壳Clap

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

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

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

打赏作者

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

抵扣说明:

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

余额充值