左神---基础提升笔记

哈希函数与哈希表等

1、设计RandomPool结构

​ 【题目】

​ 设计一种结构,在该结构中有如下三个功能:

​ insert(key):将某个key加入到该结构,做到不重复加入

​ delete(key):将原本在结构中的某个key移除

​ getRandom(): 等概率随机返回结构中的任何一个key。

​ 【要求】

​ Insert、delete和getRandom方法的时间复杂度都是O(1)

public static class Pool<K> {
		private HashMap<K, Integer> keyIndexMap;			//字符串-index  
		private HashMap<Integer, K> indexKeyMap;			//反之
		private int size;

		public Pool() {
			this.keyIndexMap = new HashMap<K, Integer>();
			this.indexKeyMap = new HashMap<Integer, K>();
			this.size = 0;
		}

		public void insert(K key) {
			if (!this.keyIndexMap.containsKey(key)) {
				this.keyIndexMap.put(key, this.size);
				this.indexKeyMap.put(this.size++, key);
			}
		}

		public void delete(K key) {
			if (this.keyIndexMap.containsKey(key)) {
				int deleteIndex = this.keyIndexMap.get(key);
				int lastIndex = --this.size;
				K lastKey = this.indexKeyMap.get(lastIndex);
				this.keyIndexMap.put(lastKey, deleteIndex);
				this.indexKeyMap.put(deleteIndex, lastKey);
				this.keyIndexMap.remove(key);
				this.indexKeyMap.remove(lastIndex);
			}
		}

		public K getRandom() {
			if (this.size == 0) {
				return null;
			}
			int randomIndex = (int) (Math.random() * this.size); // 0 ~ size -1
			return this.indexKeyMap.get(randomIndex);
		}

	}

2、详解布隆过滤器

位图:

int a = 0;
//a    32 bit
int[] arr = new int[10];
//arr[0] int 0 ~31

int i = 178;
int numIndex = 178 / 32;
int bitIndex = 178 % 32;
//拿到178位的状态
int s = ((arr[numIndex] >> (bitIndex))  & 1);

//请把178位的状态改为1
arr[numIndex] = arr[numIndex] | (1 << (bitIndex));

i = 178;//请把178位的状态改为1
arr[numIndex] = arr[numIndex] &(~  (1 << bitIndex)
public static class BitMap {

		private long[] bits;

		public BitMap(int max) {
			bits = new long[(max + 64) >> 6];
		}

		public void add(int num) {
			bits[num >> 6] |= (1L << (num & 63));
		}

		public void delete(int num) {
			bits[num >> 6] &= ~(1L << (num & 63));
		}

		public boolean contains(int num) {
			return (bits[num >> 6] & (1L << (num & 63))) != 0;
		}

	}

	public static void main(String[] args) {

		// 表示 0~ 31 谁出现了,谁没出现
//		int a = 0;
//		int num = 7;
//		// 请把7位描黑!
//		//   0000000000010000000
//		//   0000000000010000000
//		a |= 1 << 7;
//		a |= 1 << 13;
//		a |= 1 << 29;
//		// 7  13  29
//		// 请告诉我,7有没有进去
//		boolean has =( a & (1 << 7)) != 0;
//		
//		
		int[] set = new int[10];
		// set : 10个数
		// 每个数,32位
		// 0~319
		int num = 176;
		// set[0] : 0~31
		// set[1] : 32~
		int team = num / 32;
		set[team] |= 1 << (num % 32);
		
		
		
		

//		System.out.println("测试开始!");
//		int max = 2000000;
//		BitMap bitMap = new BitMap(max);
//		HashSet<Integer> set = new HashSet<>();
//		int testTime = 6000000;
//		for (int i = 0; i < testTime; i++) {
//			int num = (int) (Math.random() * (max + 1));
//			double decide = Math.random();
//			if (decide < 0.333) {
//				bitMap.add(num);
//				set.add(num);
//			} else if (decide < 0.666) {
//				bitMap.delete(num);
//				set.remove(num);
//			} else {
//				if (bitMap.contains(num) != set.contains(num)) {
//					System.out.println("Oops!");
//					break;
//				}
//			}
//		}
//		for (int num = 0; num <= max; num++) {
//			if (bitMap.contains(num) != set.contains(num)) {
//				System.out.println("Oops!");
//			}
//		}
//		System.out.println("测试结束!");
	}

考虑样本量(n)与失误率(p)

需要空间:m = - ( n * lnp) / (ln2)平方 哈希函数个数:k = ln2 * m / n ≈ 0.7 * m / n

image-20221119150027858

3、详解一致性哈希原理—虚拟节点技术

有序表与并查集等

1、岛问题

​ 一个矩阵中只有0和1两种值,每一个位置都和自己的上下左右四个位置相连如果有一片1连在一起这个部分叫做岛,这个矩阵岛屿的数目

public static int isLands(int[][] m)
{
    if(m == null || m[0] == null)
    {
        return 0;
    }
    int res = 0;
    int M = m.length;
    int N = m[0].length;
    for(int i = 0;i < M;i++)
    {
        for(int j = 0;j < i;j++)
        {
            if(a[i][j] == 1)
            {
                res++;
                infect(m,i,j,N,M);
            }
        }
    }
    return res;
}

public static void infect(int[][] m,int i,int j,int N,int M)
{
    if(i < 0 || i > N|| j < 0 || j > M || m[i][j] != 1)
    {
        return ;
    }
    m[i][j] = 2;
    infect(m,i + 1,j,N,M);
    infect(m,i - 1,j,N,M);
    infect(m,i,j + 1,N,M);
    infect(m,i,j - 1,N,M);
}

2、并查集:

public static class Node<V> {
		V value;

		public Node(V v) {
			value = v;
		}
	}

	public static class UnionFind<V> {
		public HashMap<V, Node<V>> nodes;
		public HashMap<Node<V>, Node<V>> parents;
		public HashMap<Node<V>, Integer> sizeMap;

		public UnionFind(List<V> values) {
			nodes = new HashMap<>();
			parents = new HashMap<>();
			sizeMap = new HashMap<>();
			for (V cur : values) {
				Node<V> node = new Node<>(cur);
				nodes.put(cur, node);
				parents.put(node, node);
				sizeMap.put(node, 1);
			}
		}

		// 给你一个节点,请你往上到不能再往上,把代表返回
		public Node<V> findFather(Node<V> cur) {
			Stack<Node<V>> path = new Stack<>();
			while (cur != parents.get(cur)) {
				path.push(cur);
				cur = parents.get(cur);
			}
			while (!path.isEmpty()) {
				parents.put(path.pop(), cur);
			}
			return cur;
		}

		public boolean isSameSet(V a, V b) {
			return findFather(nodes.get(a)) == findFather(nodes.get(b));
		}

		public void union(V a, V b) {
			Node<V> aHead = findFather(nodes.get(a));
			Node<V> bHead = findFather(nodes.get(b));
			if (aHead != bHead) {
				int aSetSize = sizeMap.get(aHead);
				int bSetSize = sizeMap.get(bHead);
				Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
				Node<V> small = big == aHead ? bHead : aHead;
				parents.put(small, big);
				sizeMap.put(big, aSetSize + bSetSize);
				sizeMap.remove(small);
			}
		}

		public int sets() {
			return sizeMap.size();
		}

	}

3、KMP算法

3.1 题目一

字符串str1和字符串str2,str1是否包含str2,若包含返回str2在str1中的开始位置

public static int getIndexOf(String s1, String s2) {
		if (s1 == null || s2 == null || s2.length() < 1 || s1.length() < s2.length()) {
			return -1;
		}
		char[] str1 = s1.toCharArray();
		char[] str2 = s2.toCharArray();
		int x = 0;
		int y = 0;
		// O(M) m <= n
		int[] next = getNextArray(str2);
		// O(N)
		while (x < str1.length && y < str2.length) {
			if (str1[x] == str2[y]) {
				x++;
				y++;
			} else if (next[y] == -1) { // y == 0
				x++;
			} else {
				y = next[y];
			}
		}
		return y == str2.length ? x - y : -1;
	}

	public static int[] getNextArray(char[] str2) {
		if (str2.length == 1) {
			return new int[] { -1 };
		}
		int[] next = new int[str2.length];
		next[0] = -1;
		next[1] = 0;
		int i = 2; // 目前在哪个位置上求next数组的值
		int cn = 0; // 当前是哪个位置的值再和i-1位置的字符比较
		while (i < next.length) {
			if (str2[i - 1] == str2[cn]) { // 配成功的时候
				next[i++] = ++cn;
			} else if (cn > 0) {
				cn = next[cn];
			} else {
				next[i++] = 0;
			}
		}
		return next;
	}

KMP和Manacher算法

1、Manacher算法

1.1 字符串str中,最长回文子串的长度如何求解?

public static int manacher(String s) {
		if (s == null || s.length() == 0) {
			return 0;
		}
		// "12132" -> "#1#2#1#3#2#"
		char[] str = manacherString(s);
		// 回文半径的大小
		int[] pArr = new int[str.length];
		int C = -1;
		// 讲述中:R代表最右的扩成功的位置
		// coding:最右的扩成功位置的,再下一个位置
		int R = -1;
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < str.length; i++) { // 0 1 2
			// R第一个违规的位置,i>= R
			// i位置扩出来的答案,i位置扩的区域,至少是多大。
			pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;//不用验的区域
			while (i + pArr[i] < str.length && i - pArr[i] > -1) {
				if (str[i + pArr[i]] == str[i - pArr[i]])
					pArr[i]++;
				else {
					break;
				}
			}
			if (i + pArr[i] > R) {
				R = i + pArr[i];
				C = i;
			}
			max = Math.max(max, pArr[i]);
		}
		return max - 1;
	 

	public static char[] manacherString(String str) {
		char[] charArr = str.toCharArray();
		char[] res = new char[str.length() * 2 + 1];
		int index = 0;
		for (int i = 0; i != res.length; i++) {
			res[i] = (i & 1) == 0 ? '#' : charArr[index++];
		}
		return res;
	}
    
    /*
    伪代码
    */
    for(int i = 0;i < str.length;i++)
    {
        if(i在外部){
            i暴力扩
        }else{
            if(i 的回文区域在L - R)
            {
                pArr[i] = 某个表达式
            }
            else if(回文区域有一部风在外部){
                pArr[i] = 某个表达式
            }else{i的回文区域L-R左边界重合
                以R外侧字符扩增
            }
        }
    }

2、窗口的最大值最小值更新结构

由一个代表题目,引出一种结构

【题目】

有一个整型数组 arr 和一个大小为 w 的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。

例如,数组为[4,3,5,4,3,3,6,7门,窗口大小为3时:

​ [4 3 5 ] 4 3 3 6 7

​ 4 [ 3 5 4 ] 3 3 6 7

​ 4 3 [ 5 4 3 ] 3 6 7

​ 4 3 5 [ 4 3 3 ] 6 7

​ 4 3 5 4 [ 3 3 6 ] 7

​ 4 3 5 4 3 [ 3 6 7 ]

窗口中最大值为5窗口中最大值为5窗口中最大值为5窗口中最大值为4窗口中最大值为6窗口中最大值为7

如果数组长度为 n ,窗ロ大小为 w ,则一共产生 n - w +1个窗口的最大值。

请实现一个函数。输入:整型数组 arr ,窗口大小为 W 。

输出:一个长度为 n - w +1的数组 res , res [ i ]表示每一种窗口状态下的以本题为例,结果应该返回(5,5,5,4,6.7}。

// 暴力的对数器方法
	public static int[] right(int[] arr, int w) {
		if (arr == null || w < 1 || arr.length < w) {
			return null;
		}
		int N = arr.length;
		int[] res = new int[N - w + 1];
		int index = 0;
		int L = 0;
		int R = w - 1;
		while (R < N) {
			int max = arr[L];
			for (int i = L + 1; i <= R; i++) {
				max = Math.max(max, arr[i]);

			}
			res[index++] = max;
			L++;
			R++;
		}
		return res;
	}

	public static int[] getMaxWindow(int[] arr, int w) {
		if (arr == null || w < 1 || arr.length < w) {
			return null;
		}
		// qmax 窗口最大值的更新结构
		// 放下标
		LinkedList<Integer> qmax = new LinkedList<Integer>();
		int[] res = new int[arr.length - w + 1];
		int index = 0;
		for (int R = 0; R < arr.length; R++) {
			while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[R]) {
				qmax.pollLast();
			}
			qmax.addLast(R);
			if (qmax.peekFirst() == R - w) {
				qmax.pollFirst();
			}
			if (R >= w - 1) {
				res[index++] = arr[qmax.peekFirst()];
			}
		}
		return res;
	}

3、单调栈结构

在数组(有重复值的与无重复值的)中想找到一个数,左边和右边比这个数小(大)、且离这个数最近的位置。

如果对每一个数都想求这样的信息,能不能整体代价达到O(N)?需要使用到单调栈结构

public static int[][] getNearLessNoRepeat(int[] arr) {
		int[][] res = new int[arr.length][2];
		Stack<Integer> stack = new Stack<>();
		for (int i = 0; i < arr.length; i++) {
			while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
				int popIndex = stack.pop();
				int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
				res[popIndex][0] = leftLessIndex;
				res[popIndex][1] = i;
			}
			stack.push(i);
		}
		while (!stack.isEmpty()) {
			int popIndex = stack.pop();
			int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
			res[popIndex][0] = leftLessIndex;
			res[popIndex][1] = -1;
		}
		return res;
	}

	public static int[][] getNearLess(int[] arr) {
		int[][] res = new int[arr.length][2];
		Stack<List<Integer>> stack = new Stack<>();
		for (int i = 0; i < arr.length; i++) {
			while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
				List<Integer> popIs = stack.pop();
				// 取位于下面位置的列表中,最晚加入的那个
				int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(
						stack.peek().size() - 1);
				for (Integer popi : popIs) {
					res[popi][0] = leftLessIndex;
					res[popi][1] = i;
				}
			}
			if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
				stack.peek().add(Integer.valueOf(i));
			} else {
				ArrayList<Integer> list = new ArrayList<>();
				list.add(i);
				stack.push(list);
			}
		}
		while (!stack.isEmpty()) {
			List<Integer> popIs = stack.pop();
			// 取位于下面位置的列表中,最晚加入的那个
			int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(
					stack.peek().size() - 1);
			for (Integer popi : popIs) {
				res[popi][0] = leftLessIndex;
				res[popi][1] = -1;
			}
		}
		return res;
	}

3.1题目一

定义:正数数组中累积和与最小值的乘积,假设叫做指标A。给定一个数组,请返回子数组中,指标A最大的值。

public static int max1(int[] arr) {
		int max = Integer.MIN_VALUE;
		for (int i = 0; i < arr.length; i++) {
			for (int j = i; j < arr.length; j++) {
				int minNum = Integer.MAX_VALUE;
				int sum = 0;
				for (int k = i; k <= j; k++) {
					sum += arr[k];
					minNum = Math.min(minNum, arr[k]);
				}
				max = Math.max(max, minNum * sum);
			}
		}
		return max;
	}

	public static int max2(int[] arr) {
		int size = arr.length;
		int[] sums = new int[size];
		sums[0] = arr[0];
		for (int i = 1; i < size; i++) {
			sums[i] = sums[i - 1] + arr[i];
		}
		int max = Integer.MIN_VALUE;
		Stack<Integer> stack = new Stack<Integer>();
		for (int i = 0; i < size; i++) {
			while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
				int j = stack.pop();
				max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[j]);
			}
			stack.push(i);
		}
		while (!stack.isEmpty()) {
			int j = stack.pop();
			max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]);
		}
		return max;
	}

	public static int[] gerenareRondomArray() {
		int[] arr = new int[(int) (Math.random() * 20) + 10];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) (Math.random() * 101);
		}
		return arr;
	}

滑动窗口单调栈等

第八节课 二叉树递归套路里有—1.1与1.2都有

1、树形dp套路

1.1二叉树节点间的最大距离问题

从二叉树的节点 a 出发,可以向上或者向下走,但沿途的节点只能经过一次,到达节点 b 时路径上的节点个数叫作 a 到 b 的距离,那么二叉树任何两个节点之间都有距离,求整棵树上的最大距离

public static class Node {
		public int value;
		public Node left;
		public Node right;

		public Node(int data) {
			this.value = data;
		}
	}

	public static int maxDistance(Node head) {
		int[] record = new int[1];
		return posOrder(head, record);
	}
	
	public static class ReturnType{
		public int maxDistance;
		public int h;
		
		public ReturnType(int m, int h) {
			this.maxDistance = m;;
			this.h = h;
		}
	}
	
	public static ReturnType process(Node head) {
		if(head == null) {
			return new ReturnType(0,0);
		}
		ReturnType leftReturnType = process(head.left);
		ReturnType rightReturnType = process(head.right);
        //三个可能最大值
		int includeHeadDistance = leftReturnType.h + 1 + rightReturnType.h;
		int p1 = leftReturnType.maxDistance;
		int p2 = rightReturnType.maxDistance;
        //
		int resultDistance = Math.max(Math.max(p1, p2), includeHeadDistance);
        //头结点所以+
		int hitself  = Math.max(leftReturnType.h, leftReturnType.h) + 1;
		return new ReturnType(resultDistance, hitself);
	}

	public static int posOrder(Node head, int[] record) {
		if (head == null) {
			record[0] = 0;
			return 0;
		}
		int lMax = posOrder(head.left, record);
		int maxfromLeft = record[0];
		int rMax = posOrder(head.right, record);
		int maxFromRight = record[0];
		int curNodeMax = maxfromLeft + maxFromRight + 1;
		record[0] = Math.max(maxfromLeft, maxFromRight) + 1;
		return Math.max(Math.max(lMax, rMax), curNodeMax);
	}

1.2 派对最大快乐值

派对的最大快乐值

员工信息的定义如下:

​ class Employee {

​ public int happy; // 这名员工可以带来的快乐值

​ List subordinates; // 这名员工有哪些直接下级

}

公司的每个员工都符合 Employee 类的描述。整个公司的人员结构可以看作是一棵标准的、没有环的多叉树。树的头节点是公司唯一的老板。除老板之外的每个员工都有唯一的直接上级。叶节点是没有任何下属的基层员工(subordinates列表为空),除基层员工外,每个员工都有一个或多个直接下级。

这个公司现在要办party,你可以决定哪些员工来,哪些员工不来。但是要遵循如下规则。

​ 1.如果某个员工来了,那么这个员工的所有直接下级都不能来

​ 2.派对的整体快乐值是所有到场员工快乐值的累加

​ 3.你的目标是让派对的整体快乐值尽量大

给定一棵多叉树的头节点boss,请返回派对的最大快乐值。

增强版解释:

​ 两种情况:

  1. 自己来:自己的快乐值 + 下级各个不来参加的情况下每个树的最大快乐值
  2. 自己不来:0 + Math.max(下级在来的情况下整棵树的最大值,下级在不来情况下整棵树的最大值)
public static int maxHappy(int[][] matrix) {
		int[][] dp = new int[matrix.length][2];
		boolean[] visited = new boolean[matrix.length];
		int root = 0;
		for (int i = 0; i < matrix.length; i++) {
			if (i == matrix[i][0]) {
				root = i;
			}
		}
		process(matrix, dp, visited, root);
		return Math.max(dp[root][0], dp[root][1]);
	}

	public static void process(int[][] matrix, int[][] dp, boolean[] visited, int root) {
		visited[root] = true;
		dp[root][1] = matrix[root][1];
		for (int i = 0; i < matrix.length; i++) {
			if (matrix[i][0] == root && !visited[i]) {
				process(matrix, dp, visited, i);
				dp[root][1] += dp[i][0];
				dp[root][0] += Math.max(dp[i][1], dp[i][0]);
			}
		}
	}

2、Morris遍历

​ 一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1)

​ 通过利用原树中大量空闲指针的方式,达到节省空间的目的

2.1遍历细节

Morris遍历细节
假设来到当前节点cur,开始时cur来到头节点位置
1)如果cur没有左孩子,cur向右移动(cur = cur.right)
2)如果cur有左孩子,找到左子树上最右的节点mostRight:

​ a.如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
​ b.如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)
​ 3)cur为空时遍历停止

2.2遍历

  1. ​ 先序遍历: 只过一次 直接打印 ; 过两次 第一次打印
  2. ​ 中序遍历 : 只过一次 直接打印 ; 两次第二次打印
  3. ​ 后序遍历: 逆序打印左树右边界 ; 单打整棵树右边界(逆序)

搜索二叉树:左树的值小于节点,右树的值大于节点 ; 中序遍历这棵树是升序就是的

public static class Node {
		public int value;
		Node left;
		Node right;

		public Node(int data) {
			this.value = data;
		}
	}
		//中序
	public static void morrisIn(Node head) {
		if (head == null) {
			return;
		}
		Node cur1 = head;
		Node cur2 = null;
        //3情况
		while (cur1 != null) {
			cur2 = cur1.left;
            //2情况
			if (cur2 != null) {
                
                //不断向右侧  在有指针为空  或者有指针已经指向cur停止 否则循环
				while (cur2.right != null && cur2.right != cur1) {
					cur2 = cur2.right;
				}
                //a情况
				if (cur2.right == null) {
					cur2.right = cur1;
					cur1 = cur1.left;
					continue;
				} else {
                    //b情况
					cur2.right = null;
				}
			}
			System.out.print(cur1.value + " ");
            //1情况
			cur1 = cur1.right;
		}
		System.out.println();
	}
		//先序
	public static void morrisPre(Node head) {
		if (head == null) {
			return;
		}
		Node cur1 = head;
		Node cur2 = null;
		while (cur1 != null) {
			cur2 = cur1.left;
			if (cur2 != null) {
				while (cur2.right != null && cur2.right != cur1) {
					cur2 = cur2.right;
				}
				if (cur2.right == null) {
					cur2.right = cur1;
					System.out.print(cur1.value + " ");
					cur1 = cur1.left;
					continue;
				} else {
					cur2.right = null;
				}
			} else {//无左子树
				System.out.print(cur1.value + " ");
			}
			cur1 = cur1.right;
		}
		System.out.println();
	}
	 //后序遍历
	public static void morrisPos(Node head) {
		if (head == null) {
			return;
		}
		Node cur1 = head;
		Node cur2 = null;
		while (cur1 != null) {
			cur2 = cur1.left;
			if (cur2 != null) {
				while (cur2.right != null && cur2.right != cur1) {
					cur2 = cur2.right;
				}
				if (cur2.right == null) {
					cur2.right = cur1;
					cur1 = cur1.left;
					continue;
				} else {
					cur2.right = null;
					printEdge(cur1.left);
				}
			}
			cur1 = cur1.right;
		}
		printEdge(head);
		System.out.println();
	}

	public static void printEdge(Node head) {
		Node tail = reverseEdge(head);
		Node cur = tail;
		while (cur != null) {
			System.out.print(cur.value + " ");
			cur = cur.right;
		}
		reverseEdge(tail);
	}

	public static Node reverseEdge(Node from) {
		Node pre = null;
		Node next = null;
		while (from != null) {
			next = from.right;
			from.right = pre;
			pre = from;
			from = next;
		}
		return pre;
	}

3、总结

​ 所想方法需要做第三次的信息的强整合则用二叉树的递归套路

​ 所想方法不需要第三次 最优解则可以Morris

4、大数据题目的解题技巧

​ 1)哈希函数可以把数据按照种类均匀分流

​ 2)布隆过滤器用于集合的建立与查询,并可以节省大量空间

​ 3)一致性哈希解决数据服务器的负载管理问题

​ 4)利用并查集结构做岛问题的并行计算

​ 5)位图解决某一范围上数字的出现情况,并可以节省大量空间

​ 6)利用分段统计思想、并进一步节省大量空间

​ 7)利用堆、外排序来做多个处理单元的结果合并

之前的课已经介绍过前4个内容,本节内容为介绍解决大数据题目的后3个技巧

4.1题目一

​ 32位无符号整数的范围是0~4,294,967,295,现在有一个正好包含40亿个无符号整数的文件,所以在整个范围中必然存在没出现过的数。可以使用最多1GB的内存,怎么找到所有未出现过的数?

【进阶】 内存限制为 10MB,但是只用找到一个没出现过的数即可

6 大数据题目等

1、暴力递归到动态规划

动态规划就是暴力尝试减少重复计算的技巧整,而已 

​ 这种技巧就是一个大型套路

​ 先写出用尝试的思路解决问题的递归函数,而不用操心时间复杂度

​ 这个过程是无可替代的,没有套路的,只能依靠个人智慧,或者足够多的经验

但是怎么把尝试的版本,优化成动态规划,是有固定套路的,大体步骤如下

​ 1)找到什么可变参数可以代表一个递归状态,也就是哪些参数一旦确定,返回值就确定了

​ 2)把可变参数的所有组合映射成一张表,有 1 个可变参数就是一维表,2 个可变参数就是二维表,…

​ 3)最终答案要的是表中的哪个位置,在表中标出

​ 4)根据递归过程的 base case,把这张表的最简单、不需要依赖其他位置的那些位置填好值

​ 5)根据递归过程非base case的部分,也就是分析表中的普遍位置需要怎么计算得到,那么这张表的填写顺序也就确定了

​ 6)填好表,返回最终答案在表中位置的值

1.1机器人达到指定位置方法数

【题目】

假设有排成一行的 N 个位置,记为 1~N,N 一定大于或等于2。开始时机器人在其中的M位置上(M 一定是 1~N 中的一个),机器人可以往左走或者往右走,如果机器人来到1位置,那么下一步只能往右来到 2 位置;如果机器人来到 N 位置,那么下一步只能往左来到N-1位置。规定机器人必须走 K 步,最终能来到 P 位置(P 也一定是1~N 中的一个)的方法有多少种。给定四个参数 N、M、K、P,返回方法数。 【举例】

N=5,M=2,K=3,P=3

上面的参数代表所有位置为 1 2 3 4 5。机器人最开始在2 位置上,必须经过3步,最后到达 3 位置。走的方法只有如下 3 种:

​ 1)从2到1,从1到2,从2到3

​ 2)从2到3,从3到2,从2到3

​ 3)从2到3,从3到4,从4到3

所以返回方法数 3。 N=3,M=1,K=3,P=3

上面的参数代表所有位置为 1 2 3。机器人最开始在 1 位置上,必须经过3 步,最后到达3位置。怎么走也不可能,所以返回方法数 0。

//最直接的暴力尝试 递归
public static int ways1(int N, int M, int K, int P) {
		// 参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
		// 总共N个位置,从M点出发,还剩K步,返回最终能达到P的方法数
		return walk(N, M, K, P);
	}

	// N : 位置为1 ~ N,固定参数
	// cur : 当前在cur位置,可变参数
	// rest : 还剩res步没有走,可变参数
	// P : 最终目标位置是P,固定参数
	// 该函数的含义:只能在1~N这些位置上移动,当前在cur位置,走完rest步之后,停在P位置的方法数作为返回值返回
	public static int walk(int N, int cur, int rest, int P) {
		// 如果没有剩余步数了,当前的cur位置就是最后的位置
		// 如果最后的位置停在P上,那么之前做的移动是有效的
		// 如果最后的位置没在P上,那么之前做的移动是无效的
		if (rest == 0) {
			return cur == P ? 1 : 0;
		}
		// 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2
		// 后续的过程就是,来到2位置上,还剩rest-1步要走
		if (cur == 1) {
			return walk(N, 2, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1
		// 后续的过程就是,来到N-1位置上,还剩rest-1步要走
		if (cur == N) {
			return walk(N, N - 1, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在中间位置上,那么当前这步可以走向左,也可以走向右
		// 走向左之后,后续的过程就是,来到cur-1位置上,还剩rest-1步要走
		// 走向右之后,后续的过程就是,来到cur+1位置上,还剩rest-1步要走
		// 走向左、走向右是截然不同的方法,所以总方法数要都算上
		return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);
	}

	//计划搜索   加入傻缓存
	// 1-N的位置		目标E		剩余步数S	当前位置K
	public static int walkway(int N,int E,int S,int K)
    {
		int[][] dp = new int[K + 1][N + 1];
        for(int i = 0;i <= K;i++)
        {
            for(int j = 0; <= N;j++)
            {
                dp[i][j] = -1;
            }
        }
        return f1(N,E,S,K,dp);
    }

	public static int f1(int N,int E,int rest,int cur,int[][] dp)
    {
        if(dp[rest][cur] != -1)
        {
			return dp[rest][cur];
        }
		if(rest == 0)
        {
			dp[rest][cur] = cur ==E ? 10;             
             return cur ==E ? 10;  
		}
        if(cur == 1)
        {
            dp[rest][cur] = f1(N,E,rest - 1,2,dp);   
		}else if(cur == N)
        {
             dp[rest][cur] = f1(N,E,rest - 1,cur - 1,dp); 
		}else 
        {
            dp[rest][cur] = f1(N,E,rest - 1,cur + 1,dp) + f1(N,E,rest - 1,cur - 1,dp); 
        }
        return dp[rest][cur];
    }
	public static int ways2(int N, int M, int K, int P) {
		// 参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
		int[][] dp = new int[K + 1][N + 1];
		dp[0][P] = 1;
		for (int i = 1; i <= K; i++) {
			for (int j = 1; j <= N; j++) {
				if (j == 1) {
					dp[i][j] = dp[i - 1][2];
				} else if (j == N) {
					dp[i][j] = dp[i - 1][N - 1];
				} else {
					dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];
				}
			}
		}
		return dp[K][M];
	}

	public static int ways3(int N, int M, int K, int P) {
		// 参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
		int[] dp = new int[N + 1];
		dp[P] = 1;
		for (int i = 1; i <= K; i++) {
			int leftUp = dp[1];// 左上角的值
			for (int j = 1; j <= N; j++) {
				int tmp = dp[j];
				if (j == 1) {
					dp[j] = dp[j + 1];
				} else if (j == N) {
					dp[j] = leftUp;
				} else {
					dp[j] = leftUp + dp[j + 1];
				}
				leftUp = tmp;
			}
		}
		return dp[M];
	}

2、换钱的最少货币数

2.1题目一

【题目】

给定数组 arr,arr 中所有的值代表硬币的面值可以重复。每一个值代表一枚硬币,给定一个整数 aim,代表要找的钱数,求组成aim的最少硬币数。

暴力递归:

 public static int minCoins1(int[] arr, int aim) {
 	process(arr,0,aim);
 }
public static int process(int [] arr,int index,int rest)
{
    if(rest < 0)
    {
        return -1;
    }
    if(rest == 0)
    {
        return 0;
    }
    if(index == arr.length)
    {
        return -1;
    }
    //rest > 0而且有银币
    int p1 = process(arr,index + 1,rest);
    int p2Next = process(arr,index + 1,rest - arr[index]);
    if(p1 == -1 && p2Next == -1)
    {
        return -1;
    }else{
        if(p1 == -1)
        {
            return p2Next + 1;
        }
        if(p2Next == -1)
        {
            return p1;
        }
        return Math.min(p1,p2Next+ 1);
    }
}

计划搜索:

 public static int minCoins1(int[] arr, int aim) {
     int[][] dp = new int[arr.length + 1][aim + 1];
        for(int i = 0;i <= arr.length;i++)
        {
            for(int j = 0; <= aim;j++)
            {
                dp[i][j] = -2;
            }
        }
 	process2(arr,0,aim);
 }
public static int process2(int [] arr,int index,int rest,int[][] dp)
{
    if(rest < 0)
    {
        return -1;
    }
    if(dp[index][rest] != -2)
    {
        return dp[index][rest];
    }
    if(rest == 0)
    {
        dp[index][rest] = 0;
    }else if(index == arr.length){
        dp[index][rest] = -1;
    }else{
         int p1 = process2(arr,index + 1,rest,dp);
    	int p2Next = process2(arr,index + 1,rest - arr[index],dp);
    	if(p1 == -1 && p2Next == -1)
    	{
       	 	dp[index][rest] =  -1;
    	}else{
        	if(p1 == -1)
        	{
            	dp[index][rest] =  p2Next + 1;
        	}
        	if(p2Next == -1)
        	{
            	dp[index][rest] =  p1;
        	}else
        	{
            	dp[index][rest] = Math.min(p1,p2Next+ 1);
        	}     
    	}
	}
    return    dp[index][rest];
}

dp:

public static int minCoins1(int[] arr, int aim) {
	int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1]; 
    for(int row = 0; row <= N;row++)
    {
        dp[row][0] = 0;
    }
    for(int col = 1;col <= aim;col++)
    {
        dp[N][col] = -1;
    }
    for(int index = N - 1;index >= 0;index--)
    {
        for(int rest = 1;rest <= aim;rest++)
        {
            int p1 = dp[index + 1][rest];
            int p2Next = -1;
            if(rest - arr[index] >= 0)
            {
                p2Next = dp[index + 1][rest - arr[index]]
			}
            if(p1 == -1 && p2 Next == -1)
            {
                dp[index][rest] = -1;
            }else{
                if(p1 == -1)
                {
                    dp[index][rest] = p2Next + 1;
                }
                if(p2Next == -1)
                {
                    dp[index][rest] = p1;
				}
                dp[index][rest] = Math.min(p1,p2Next + 1);
            }
        }
    }
    return dp[0][aim];
}

2.2题目二

题目】

给定数组 arr,arr 中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数 aim,代表要找的钱数,求组成aim的最少货币数。

【举例】

arr=[5,2,3],aim=20。

4 张 5 元可以组成 20 元,其他的找钱方案都要使用更多张的货币,所以返回4。

arr=[5,2,3],aim=0。

不用任何货币就可以组成 0 元,返回 0。

arr=[3,5],aim=2。

根本无法组成 2 元,钱不能找开的情况下默认返回-1。

 public static int minCoins1(int[] arr, int aim) {
		if (arr == null || arr.length == 0 || aim < 0) {
			return -1;
		}
		return process(arr, 0, aim);
	}

	// 当前考虑的面值是arr[i],还剩rest的钱需要找零
	// 如果返回-1说明自由使用arr[i..N-1]面值的情况下,无论如何也无法找零rest
	// 如果返回不是-1,代表自由使用arr[i..N-1]面值的情况下,找零rest需要的最少张数
	public static int process(int[] arr, int i, int rest) {
		// base case:
		// 已经没有面值能够考虑了
		// 如果此时剩余的钱为0,返回0张
		// 如果此时剩余的钱不是0,返回-1
		if (i == arr.length) {
			return rest == 0 ? 0 : -1;
		}
		// 最少张数,初始时为-1,因为还没找到有效解
		int res = -1;
		// 依次尝试使用当前面值(arr[i])0张、1张、k张,但不能超过rest
		for (int k = 0; k * arr[i] <= rest; k++) {
			// 使用了k张arr[i],剩下的钱为rest - k * arr[i]
			// 交给剩下的面值去搞定(arr[i+1..N-1])
			int next = process(arr, i + 1, rest - k * arr[i]);
			if (next != -1) { // 说明这个后续过程有效
				res = res == -1 ? next + k : Math.min(res, next + k);
			}
		}
		return res;
	}

	public static int minCoins2(int[] arr, int aim) {
		if (arr == null || arr.length == 0 || aim < 0) {
			return -1;
		}
		int N = arr.length;
		int[][] dp = new int[N + 1][aim + 1];
		// 设置最后一排的值,除了dp[N][0]为0之外,其他都是-1
		for (int col = 1; col <= aim; col++) {
			dp[N][col] = -1;
		}
		for (int i = N - 1; i >= 0; i--) { // 从底往上计算每一行
			for (int rest = 0; rest <= aim; rest++) { // 每一行都从左往右
				dp[i][rest] = -1; // 初始时先设置dp[i][rest]的值无效
				if (dp[i + 1][rest] != -1) { // 下面的值如果有效
					dp[i][rest] = dp[i + 1][rest]; // dp[i][rest]的值先设置成下面的值
				}
				// 左边的位置不越界并且有效
				if (rest - arr[i] >= 0 && dp[i][rest - arr[i]] != -1) {
					if (dp[i][rest] == -1) { // 如果之前下面的值无效
						dp[i][rest] = dp[i][rest - arr[i]] + 1;
					} else { // 说明下面和左边的值都有效,取最小的
						dp[i][rest] = Math.min(dp[i][rest],
								dp[i][rest - arr[i]] + 1);
					}
				}
			}
		}
		return dp[0][aim];
	}

7、暴力递归上

1.1排成一条线的纸牌博弈问题

【题目】

给定一个整型数组 arr,代表数值不同的纸牌排成一条线。玩家A 和玩家B 依次拿走每张纸牌,规定玩家 A 先拿,玩家 B 后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家 B 都绝顶聪明。请返回最后获胜者的分数。

【举例】

arr=[1,2,100,4]。

开始时,玩家 A 只能拿走 1 或 4。如果玩家 A 拿走 1,则排列变为[2,100,4],接下来玩家B可以拿走 2 或 4,然后继续轮到玩家 A。如果开始时玩家A 拿走4,则排列变为[1,2,100],接下 来玩家 B 可以拿走 1 或 100,然后继续轮到玩家 A。玩家A 作为绝顶聪明的人不会先拿4,因为 拿 4 之后,玩家 B 将拿走 100。所以玩家 A 会先拿1,让排列变为[2,100,4],接下来玩家 B 不管 怎么选,100 都会被玩家 A 拿走。玩家 A 会获胜,分数为101。所以返回101。arr=[1,100,2]。

开始时,玩家 A 不管拿 1 还是 2,玩家 B 作为绝顶聪明的人,都会把100 拿走。玩家B会获胜,分数为 100。所以返回 100。

首先博弈的先后手问题 是比较难以考虑的,先手函数调用的后手函数,后手函数调用的先手函数,而且 要明白先后手的情况是相对而言的,继而我们在改动暴力递归到dp的时候就要考虑缓存的先手dp的数组缓存后手信息,反之一样。

他也是范围性的尝试 正方形,左下半部分 无效

//暴力尝试
public static int win1(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
	}
	//先手函数
	public static int f(int[] arr, int i, int j) {
		if (i == j) {
			return arr[i];
		}
		return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
	}
	//后手函数
	public static int s(int[] arr, int i, int j) {
		if (i == j) {
			return 0;
		}
		return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
	}
 
	public static int win2(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		int[][] f = new int[arr.length][arr.length];
		int[][] s = new int[arr.length][arr.length];
		for (int j = 0; j < arr.length; j++) {
			f[j][j] = arr[j];
			for (int i = j - 1; i >= 0; i--) {
				f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
				s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
			}
		}
		return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
	}

1.2象棋中马的跳法

【题目】

请同学们自行搜索或者想象一个象棋的棋盘,然后把整个棋盘放入第一象限,棋盘的最左下角是(0,0)位置。那么整个棋盘就是横坐标上9条线、纵坐标上10条线的一个区域。给你三个参数,x,y,k,返回如果“马”从(0,0)位置出发,必须走k步,最后落在(x,y)上的方法数有多少种?

考虑:由于象棋的特殊性,他必须以特定的位置才能到达x,y 那么以x,y 反推需要到达那个位置才能到达x,y 则下述的函数的八个位置才能到达。base case 很好确定 递归暴力很好实现

public static int getWays(int x, int y, int step) {
		return process(x, y, step);
	}
	//目的地:x,y位置   步数step
	//返回方法数目
	public static int process(int x, int y, int step) {
		if (x < 0 || x > 8 || y < 0 || y > 9) {
			return 0;
		}
		if (step == 0) {
			return (x == 0 && y == 0) ? 1 : 0;
		}
		return process(x - 1, y + 2, step - 1)
				+ process(x + 1, y + 2, step - 1)
				+ process(x + 2, y + 1, step - 1)
				+ process(x + 2, y - 1, step - 1)
				+ process(x + 1, y - 2, step - 1)
				+ process(x - 1, y - 2, step - 1)
				+ process(x - 2, y - 1, step - 1)
				+ process(x - 2, y + 1, step - 1);
	}

	public static int dpWays(int x, int y, int step) {
		if (x < 0 || x > 8 || y < 0 || y > 9 || step < 0) {
			return 0;
		}
		int[][][] dp = new int[9][10][step + 1];
		dp[0][0][0] = 1;
		for (int h = 1; h <= step; h++) {
			for (int r = 0; r < 9; r++) {
				for (int c = 0; c < 10; c++) {
					dp[r][c][h] += getValue(dp, r - 1, c + 2, h - 1);
					dp[r][c][h] += getValue(dp, r + 1, c + 2, h - 1);
					dp[r][c][h] += getValue(dp, r + 2, c + 1, h - 1);
					dp[r][c][h] += getValue(dp, r + 2, c - 1, h - 1);
					dp[r][c][h] += getValue(dp, r + 1, c - 2, h - 1);
					dp[r][c][h] += getValue(dp, r - 1, c - 2, h - 1);
					dp[r][c][h] += getValue(dp, r - 2, c - 1, h - 1);
					dp[r][c][h] += getValue(dp, r - 2, c + 1, h - 1);
				}
			}
		}
		return dp[x][y][step];
	}

	public static int getValue(int[][][] dp, int row, int col, int step) {
		if (row < 0 || row > 8 || col < 0 || col > 9) {
			return 0;
		}
		return dp[row][col][step];
	}

1.3Bob的生存概率

【题目】

给定五个参数n,m,i,j,k。表示在一个N*M的区域,Bob处在(i,j)点,每次Bob等概率的向上、下、左、右四个方向移动一步,Bob必须走K步。如果走完之后,Bob还停留在这个区域上,就算Bob存活,否则就算Bob死亡。请求解Bob的生存概率,返回字符串表示分数的方式。

public static String bob1(int N, int M, int i, int j, int K) {
		long all = (long) Math.pow(4, K);
		long live = process(N, M, i, j, K);
		long gcd = gcd(all, live);
		return String.valueOf((live / gcd) + "/" + (all / gcd));
	}

	public static long process(int N, int M, int row, int col, int rest) {
		if (row < 0 || row == N || col < 0 || col == M) {
			return 0;
		}
		if (rest == 0) {
			return 1;
		}
		long live = process(N, M, row - 1, col, rest - 1);
		live += process(N, M, row + 1, col, rest - 1);
		live += process(N, M, row, col - 1, rest - 1);
		live += process(N, M, row, col + 1, rest - 1);
		return live;
	}

	public static long gcd(long m, long n) {
		return n == 0 ? m : gcd(n, m % n);
	}

	public static String bob2(int N, int M, int i, int j, int K) {
		int[][][] dp = new int[N + 2][M + 2][K + 1];
		for (int row = 1; row <= N; row++) {
			for (int col = 1; col <= M; col++) {
				dp[row][col][0] = 1;
			}
		}
		for (int rest = 1; rest <= K; rest++) {
			for (int row = 1; row <= N; row++) {
				for (int col = 1; col <= M; col++) {
					dp[row][col][rest] = dp[row - 1][col][rest - 1];
					dp[row][col][rest] += dp[row + 1][col][rest - 1];
					dp[row][col][rest] += dp[row][col - 1][rest - 1];
					dp[row][col][rest] += dp[row][col + 1][rest - 1];
				}
			}
		}
		long all = (long) Math.pow(4, K);
		long live = dp[i + 1][j + 1][K];
		long gcd = gcd(all, live);
		return String.valueOf((live / gcd) + "/" + (all / gcd));
	}

2、第六节课的2.2题目

出现枚举类型的优化题目

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值