From cecb9d6354924403ca55441e0aeff024b862786c Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Wed, 23 Mar 2022 21:40:42 +0800 Subject: [PATCH 01/18] kmp and bfprt --- ...4\270\216bfprt\347\256\227\346\263\225.md" | 473 +++++++++--------- 1 file changed, 241 insertions(+), 232 deletions(-) diff --git "a/15-\343\200\212\350\277\233\351\230\266\343\200\213KMP\347\256\227\346\263\225\344\270\216bfprt\347\256\227\346\263\225.md" "b/15-\343\200\212\350\277\233\351\230\266\343\200\213KMP\347\256\227\346\263\225\344\270\216bfprt\347\256\227\346\263\225.md" index 1491219..a3b11d0 100644 --- "a/15-\343\200\212\350\277\233\351\230\266\343\200\213KMP\347\256\227\346\263\225\344\270\216bfprt\347\256\227\346\263\225.md" +++ "b/15-\343\200\212\350\277\233\351\230\266\343\200\213KMP\347\256\227\346\263\225\344\270\216bfprt\347\256\227\346\263\225.md" @@ -1,8 +1,6 @@ [TOC] -# 1 KMP算法 - -`大厂劝退,面试高频^_^` +# 1 KMP算法(面试高频,劝退) ## 1.1 KMP算法分析 @@ -26,7 +24,7 @@ 5、对于k位置前的字符,前后缀长度取5时,前缀为"abcab"后缀为"bcabc"不相等 -==注意前后缀长度不可取k位置前的整体长度6。那么此时k位置前的最大匹配长度为3== +> 注意前后缀长度不可取k位置前的整体长度6。那么此时k位置前的最大匹配长度为3 所以,例如"aaaaaab","b"的坐标为6,那么"b"坐标前的前后缀最大匹配长度为5 @@ -46,96 +44,87 @@ Code: -```Java -public class Code01_KMP { - // O(N) - public static int getIndexOf(String s, String m) { - if (s == null || m == null || m.length() < 1 || s.length() < m.length()) { - return -1; - } - char[] str = s.toCharArray(); - char[] match = m.toCharArray(); - int x = 0; // str中当前比对到的位置 - int y = 0; // match中当前比对到的位置 - // match的长度M,M <= N O(M) - int[] next = getNextArray(match); // next[i] match中i之前的字符串match[0..i-1],最长前后缀相等的长度 - // O(N) - // x在str中不越界,y在match中不越界 - while (x < str.length && y < match.length) { - // 如果比对成功,x和y共同往各自的下一个位置移动 - if (str[x] == match[y]) { - x++; - y++; - } else if (next[y] == -1) { // 表示y已经来到了0位置 y == 0 - // str换下一个位置进行比对 - x++; - } else { // y还可以通过最大前后缀长度往前移动 - y = next[y]; - } - } - // 1、 x越界,y没有越界,找不到,返回-1 - // 2、 x没越界,y越界,配出 - // 3、 x越界,y越界 ,配出,str的末尾,等于match - // 只要y越界,就配出了,配出的位置等于str此时所在的位置x,减去y的长度。就是str存在匹配的字符串的开始位置 - return y == match.length ? x - y : -1; +```Go +package main + +import "fmt" + +// getIndexOf O(N) +func getIndexOf(s string, m string) int { + if len(s) == 0 || len(m) == 0 || len(s) < len(m) { + return -1 } - // M O(M) - public static int[] getNextArray(char[] match) { - // 如果match只有一个字符,人为规定-1 - if (match.length == 1) { - return new int[] { -1 }; - } - // match不止一个字符,人为规定0位置是-1,1位置是0 - int[] next = new int[match.length]; - next[0] = -1; - next[1] = 0; - int i = 2; - // cn代表,cn位置的字符,是当前和i-1位置比较的字符 - int cn = 0; - while (i < next.length) { - if (match[i - 1] == match[cn]) { // 跳出来的时候 - // next[i] = cn+1; - // i++; - // cn++; - // 等同于 - next[i++] = ++cn; - // 跳失败,如果cn>0说明可以继续跳 - } else if (cn > 0) { - cn = next[cn]; - // 跳失败,跳到开头仍然不等 - } else { - next[i++] = 0; - } + str := []byte(s) + match := []byte(m) + x := 0 // str中当前比对到的位置 + y := 0 // match中当前比对到的位置 + // match的长度M,M <= N O(M) + next := getNextArray(match) // next[i] match中i之前的字符串match[0..i-1],最长前后缀相等的长度 + // O(N) + // x在str中不越界,y在match中不越界 + for x < len(str) && y < len(match) { + // 如果比对成功,x和y共同往各自的下一个位置移动 + if str[x] == match[y] { + x++ + y++ + } else if next[y] == -1 { // 表示y已经来到了0位置 y == 0 + // str换下一个位置进行比对 + x++ + } else { // y还可以通过最大前后缀长度往前移动 + y = next[y] } - return next; } + // 1、 x越界,y没有越界,找不到,返回-1 + // 2、 x没越界,y越界,配出 + // 3、 x越界,y越界 ,配出,str的末尾,等于match + // 只要y越界,就配出了,配出的位置等于str此时所在的位置x,减去y的长度。就是str存在匹配的字符串的开始位置 + if y == len(match) { + return x - y + } else { + return -1 + } +} - // for test - public static String getRandomString(int possibilities, int size) { - char[] ans = new char[(int) (Math.random() * size) + 1]; - for (int i = 0; i < ans.length; i++) { - ans[i] = (char) ((int) (Math.random() * possibilities) + 'a'); - } - return String.valueOf(ans); +// M O(M) +func getNextArray(match []byte) []int { + // 如果match只有一个字符,人为规定-1 + if len(match) == 1 { + return []int{-1} } - public static void main(String[] args) { - int possibilities = 5; - int strSize = 20; - int matchSize = 5; - int testTimes = 5000000; - System.out.println("test begin"); - for (int i = 0; i < testTimes; i++) { - String str = getRandomString(possibilities, strSize); - String match = getRandomString(possibilities, matchSize); - if (getIndexOf(str, match) != str.indexOf(match)) { - System.out.println("Oops!"); - } + // match不止一个字符,人为规定0位置是-1,1位置是0 + next := make([]int, len(match)) + + next[0] = -1 + next[1] = 0 + + i := 2 + // cn代表,cn位置的字符,是当前和i-1位置比较的字符 + cn := 0 + for i < len(next) { + if match[i - 1] == match[cn] { // 跳出来的时候 + // next[i] = cn+1 + // i++ + // cn++ + // 等同于 + cn++ + next[i] = cn + i++ + } else if cn > 0 { // 跳失败,如果cn>0说明可以继续跳 + cn = next[cn] + } else { // 跳失败,跳到开头仍然不等 + next[i] = 0 + i++ } - System.out.println("test finish"); } + return next +} +func main() { + s := "abc1234efd" + m := "1234" + fmt.Println(getIndexOf(s, m)) } ``` @@ -157,9 +146,7 @@ KMP解法:str1拼接str1得到str',"123456123456",我们看str2是否是str' -# 2 bfprt算法 - -`面试常见` +# 2 bfprt算法 (面试常见) 情形:在一个无序数组中,怎么求第k小的数。如果通过排序,那么排序的复杂度为O(n*logn)。问,如何O(N)复杂度解决这个问题? @@ -218,183 +205,205 @@ T(N) = T(N/5) + T(7n/10) + O(N) > bfprt算法在算法上的地位非常高,它发现只要涉及到我们随便定义的一个常数分组,得到一个表达式,最后收敛到O(N),那么就可以通过O(N)的复杂度测试 -```Java -public class Code01_FindMinKth { +```Go +package main - public static class MaxHeapComparator implements Comparator { +import ( + "container/heap" + "fmt" + "math" + "math/rand" + "time" +) - @Override - public int compare(Integer o1, Integer o2) { - return o2 - o1; - } +type Heap []int - } +func (h Heap) Less(i, j int) bool { + return h[i] > h[j] // 大根堆。小根堆实现为: h[i] <= h[j] +} - // 利用大根堆,时间复杂度O(N*logK) - public static int minKth1(int[] arr, int k) { - PriorityQueue maxHeap = new PriorityQueue<>(new MaxHeapComparator()); - for (int i = 0; i < k; i++) { - maxHeap.add(arr[i]); - } - for (int i = k; i < arr.length; i++) { - if (arr[i] < maxHeap.peek()) { - maxHeap.poll(); - maxHeap.add(arr[i]); - } - } - return maxHeap.peek(); - } +func (h Heap) Len() int { + return len(h) +} - // 改写快排,时间复杂度O(N) - public static int minKth2(int[] array, int k) { - int[] arr = copyArray(array); - return process2(arr, 0, arr.length - 1, k - 1); - } +func (h Heap) Swap(i, j int) { + h[i], h[j] = h[j], h[i] +} - public static int[] copyArray(int[] arr) { - int[] ans = new int[arr.length]; - for (int i = 0; i != ans.length; i++) { - ans[i] = arr[i]; - } - return ans; +func (h *Heap) Push(v interface{}) { + *h = append(*h, v.(int)) +} + +func (h *Heap) Pop() interface{} { + n := len(*h) + x := (*h)[n-1] + *h = (*h)[:n-1] + return x +} + +// minKth1 找一个数组中第k小的数。方法1:利用大根堆,时间复杂度O(N*logK) +func minKth1(arr []int, k int) int { + maxHeap := &Heap{} + for i := 0; i < k; i++ { // 加入大根堆 + heap.Push(maxHeap, arr[i]) } + heap.Init(maxHeap) - // arr 第k小的数: process2(arr, 0, N-1, k-1) - // arr[L..R] 范围上,如果排序的话(不是真的去排序),找位于index的数 - // index [L..R] - // 通过荷兰国旗的优化,概率期望收敛于O(N) - public static int process2(int[] arr, int L, int R, int index) { - if (L == R) { // L == R ==INDEX - return arr[L]; - } - // 不止一个数 L + [0, R -L],随机选一个数 - int pivot = arr[L + (int) (Math.random() * (R - L + 1))]; - - // 返回以pivot为划分值的中间区域的左右边界 - // range[0] range[1] - // L ..... R pivot - // 0 1000 70...800 - int[] range = partition(arr, L, R, pivot); - // 如果我们第k小的树正好在这个范围内,返回区域的左边界 - if (index >= range[0] && index <= range[1]) { - return arr[index]; - // index比该区域的左边界小,递归左区间 - } else if (index < range[0]) { - return process2(arr, L, range[0] - 1, index); - // index比该区域的右边界大,递归右区间 - } else { - return process2(arr, range[1] + 1, R, index); + for i := k; i < len(arr); i++ { + if arr[i] < (*maxHeap)[0] { // arr[i] 小于堆顶元素。堆顶元素就是0位置元素 + // !!! 这里一定要使用系统包中的pop和push,然后把实现当前栈接口的结构传入 + heap.Pop(maxHeap) // 弹出 + heap.Push(maxHeap, arr[i]) // 入堆 } } - public static int[] partition(int[] arr, int L, int R, int pivot) { - int less = L - 1; - int more = R + 1; - int cur = L; - while (cur < more) { - if (arr[cur] < pivot) { - swap(arr, ++less, cur++); - } else if (arr[cur] > pivot) { - swap(arr, cur, --more); - } else { - cur++; - } - } - return new int[] { less + 1, more - 1 }; + // return maxHeap.peek() + return (*maxHeap)[0] +} + +// minKth2 找一个数组中第k小的数。方法2:利用快排,时间复杂度O(N) +func minKth2(array []int, k int) int { + arr := copyArr(array) + return process2(arr, 0, len(arr)-1, k-1) +} + +// copyArr 克隆数组,防止快排影响原数组的元素顺序 +func copyArr(arr []int) []int { + ans := make([]int, len(arr)) + for i := 0; i < len(ans); i++ { // 这里copy数组,不可以使用append。 + ans[i] = arr[i] } + return ans +} - public static void swap(int[] arr, int i1, int i2) { - int tmp = arr[i1]; - arr[i1] = arr[i2]; - arr[i2] = tmp; +// arr 第k小的数: process2(arr, 0, N-1, k-1) +// arr[L..R] 范围上,如果排序的话(不是真的去排序),找位于index的数 +// index [L..R] +// 通过荷兰国旗的优化,概率期望收敛于O(N) +func process2(arr []int, L, R, index int) int { + if L == R { // L == R ==INDEX + return arr[L] } - // 利用bfprt算法,时间复杂度O(N) - public static int minKth3(int[] array, int k) { - int[] arr = copyArray(array); - return bfprt(arr, 0, arr.length - 1, k - 1); + // 不止一个数 L + [0, R -L],随机选一个数. + pivot := arr[L+rand.Intn(R-L)] + + // 返回以pivot为划分值的中间区域的左右边界 + // range[0] range[1] + // L ..... R pivot + // 0 1000 70...800 + rg := partition(arr, L, R, pivot) + // 如果我们第k小的树正好在这个范围内,返回区域的左边界 + if index >= rg[0] && index <= rg[1] { + return arr[index] + } else if index < rg[0] { // index比该区域的左边界小,递归左区间 + return process2(arr, L, rg[0]-1, index) + } else { // index比该区域的右边界大,递归右区间 + return process2(arr, rg[1]+1, R, index) } +} - // arr[L..R] 如果排序的话,位于index位置的数,是什么,返回 - public static int bfprt(int[] arr, int L, int R, int index) { - if (L == R) { - return arr[L]; - } - // 通过bfprt分组,最终选出m。不同于随机选择m作为划分值 - int pivot = medianOfMedians(arr, L, R); - int[] range = partition(arr, L, R, pivot); - if (index >= range[0] && index <= range[1]) { - return arr[index]; - } else if (index < range[0]) { - return bfprt(arr, L, range[0] - 1, index); +// partition 荷兰国旗partition问题 +func partition(arr []int, L, R, pivot int) []int { + less := L - 1 + more := R + 1 + cur := L + for cur < more { + if arr[cur] < pivot { + less++ + arr[less], arr[cur] = arr[cur], arr[less] + cur++ + } else if arr[cur] > pivot { + more-- + arr[cur], arr[more] = arr[more], arr[cur] } else { - return bfprt(arr, range[1] + 1, R, index); + cur++ } } + return []int{less + 1, more - 1} +} - // arr[L...R] 五个数一组 - // 每个小组内部排序 - // 每个小组中位数拿出来,组成marr - // marr中的中位数,返回 - public static int medianOfMedians(int[] arr, int L, int R) { - int size = R - L + 1; - // 是否需要补最后一组,例如13,那么需要补最后一组,最后一组为3个数 - int offset = size % 5 == 0 ? 0 : 1; - int[] mArr = new int[size / 5 + offset]; - for (int team = 0; team < mArr.length; team++) { - int teamFirst = L + team * 5; - // L ... L + 4 - // L +5 ... L +9 - // L +10....L+14 - mArr[team] = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4)); - } - // marr中,找到中位数,原问题是arr拿第k小的数,这里是中位数数组拿到中间位置的数(第mArr.length / 2小的数),相同的问题 - // 返回值就是我们需要的划分值m - // marr(0, marr.len - 1, mArr.length / 2 ) - return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2); +// minKth3 找一个数组中第k小的数。方法3:利用bfprt算法,时间复杂度O(N) +func minKth3(array []int, k int) int { + arr := copyArr(array) + return bfprt(arr, 0, len(arr)-1, k-1) +} + +// bfprt arr[L..R] 如果排序的话,位于index位置的数,是什么,返回 +func bfprt(arr []int, L, R, index int) int { + if L == R { + return arr[L] } - public static int getMedian(int[] arr, int L, int R) { - insertionSort(arr, L, R); - return arr[(L + R) / 2]; + // 通过bfprt分组,最终选出m。不同于随机选择m作为划分值 + pivot := medianOfMedians(arr, L, R) + rg := partition(arr, L, R, pivot) + if index >= rg[0] && index <= rg[1] { + return arr[index] + } else if index < rg[0] { + return bfprt(arr, L, rg[0]-1, index) + } else { + return bfprt(arr, rg[1]+1, R, index) } +} - // 由于确定是5个数排序,我们选择一个常数项最低的排序-插入排序 - public static void insertionSort(int[] arr, int L, int R) { - for (int i = L + 1; i <= R; i++) { - for (int j = i - 1; j >= L && arr[j] > arr[j + 1]; j--) { - swap(arr, j, j + 1); - } - } +// arr[L...R] 五个数一组 +// 每个小组内部排序 +// 每个小组中位数拿出来,组成marr +// marr中的中位数,返回 +func medianOfMedians(arr []int, L, R int) int { + size := R - L - L + // 是否需要补最后一组,例如13,那么需要补最后一组,最后一组为3个数 + offset := -1 + if size%5 == 0 { + offset = 0 + } else { + offset = 1 } - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) (Math.random() * maxSize) + 1]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) (Math.random() * (maxValue + 1)); - } - return arr; + // 初始化数组 + mArr := make([]int, size/5+offset) + for team := 0; team < len(mArr); team++ { + teamFirst := L + team*5 + // L ... L + 4 + // L +5 ... L +9 + // L +10....L+14 + mArr[team] = getMedian(arr, teamFirst, int(math.Min(float64(R), float64(teamFirst+4)))) } - public static void main(String[] args) { - int testTime = 1000000; - int maxSize = 100; - int maxValue = 100; - System.out.println("test begin"); - for (int i = 0; i < testTime; i++) { - int[] arr = generateRandomArray(maxSize, maxValue); - int k = (int) (Math.random() * arr.length) + 1; - int ans1 = minKth1(arr, k); - int ans2 = minKth2(arr, k); - int ans3 = minKth3(arr, k); - if (ans1 != ans2 || ans2 != ans3) { - System.out.println("Oops!"); - } + // marr中,找到中位数,原问题是arr拿第k小的数,这里是中位数数组拿到中间位置的数(第mArr.length / 2小的数),相同的问题 + // 返回值就是我们需要的划分值m + // marr(0, marr.len - 1, mArr.length / 2 ) + return bfprt(mArr, 0, len(mArr)-1, len(mArr)/2) +} + +func getMedian(arr []int, L, R int) int { + insertionSort(arr, L, R) + return arr[(L+R)/2] +} + +// insertionSort 插入排序 +func insertionSort(arr []int, L, R int) { + for i := L + 1; i <= R; i++ { + for j := i - 1; j >= L && arr[j] > arr[j+1]; j-- { + arr[j], arr[j+1] = arr[j+1], arr[j] } - System.out.println("test finish"); } +} +func main() { + /* + rand.Seed: + 还函数是用来创建随机数的种子,如果不执行该步骤创建的随机数是一样的,因为默认Go会使用一个固定常量值来作为随机种子。 + + time.Now().UnixNano(): + 当前操作系统时间的毫秒值 + */ + rand.Seed(time.Now().UnixNano()) + arr := []int{3, 4, 6, 1, 77, 35, 26, 83, 56, 37} + fmt.Println(minKth1(arr, 3)) + fmt.Println(minKth2(arr, 3)) + fmt.Println(minKth3(arr, 3)) } ``` From 770b179c5e8a39e7c2ac01a48701cbcd74a9ef12 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Mon, 28 Mar 2022 22:00:17 +0800 Subject: [PATCH 02/18] manacher + --- ...1\350\275\246)\347\256\227\346\263\225.md" | 252 +++++++++--------- 1 file changed, 125 insertions(+), 127 deletions(-) diff --git "a/16-\343\200\212\350\277\233\351\230\266\343\200\213Manacher(\351\251\254\346\213\211\350\275\246)\347\256\227\346\263\225.md" "b/16-\343\200\212\350\277\233\351\230\266\343\200\213Manacher(\351\251\254\346\213\211\350\275\246)\347\256\227\346\263\225.md" index 3b31b6f..f67cd5d 100644 --- "a/16-\343\200\212\350\277\233\351\230\266\343\200\213Manacher(\351\251\254\346\213\211\350\275\246)\347\256\227\346\263\225.md" +++ "b/16-\343\200\212\350\277\233\351\230\266\343\200\213Manacher(\351\251\254\346\213\211\350\275\246)\347\256\227\346\263\225.md" @@ -13,7 +13,9 @@ Manacher算法解决在一个字符串中最长回文子串的大小,例如"ab ## 1.2 字符串最长回文子串暴力解 -遍历str,以i为回文对称的回文串有多长,在i位置左右扩展。所以字符串"abc123df" +> 扩散法 + +遍历str,以i为回文对称的回文串有多长,在i位置左右扩展。如果字符串"abc123df" i为0的时候,回文串为"a",长度为1; @@ -42,7 +44,7 @@ i为8的时候,回文串为"#1#2#1#1#2#1#",长度为13; 复杂度最差情况是,所有字符长度为n,且所有字符都相等。经过我们的填充,字符串规模扩展到2n+1。每次寻找,都会寻找到左边界或者右边界,该方法的事件复杂度为O(N*N) -Manacher算法解决该类问题,O(N)复杂度! +Manacher算法解该类问题,O(N)复杂度可以解决 ## 1.3 Manacher解决最长回文串O(N) @@ -63,114 +65,90 @@ Manacher算法的核心概念就是,回文半径数组pArr[],回文最右边 在遍历填充数组时,会出现i在R外,和i在R内两种情况。 -当i在R外时,没有任何优化,继续遍历去寻找。当i在R内涉及到Manacher算法的优化。i在R内的时候,i肯定大于C,我们可以根据R和C求出左边界L,也可以根据i和C求出以C堆成的i'。 +当i在R外时,没有任何优化,继续遍历去寻找。当i在R内涉及到Manacher算法的优化。i在R内的时候,i肯定大于C,我们可以根据R和C求出左边界L,也可以根据i和C求出以C对称的i'。 -- 情况1:i'的回文区域在彻底在L和R的内部。i不需要再求回文串,i的回文串的大小等于pArr[]中i'位置的长度。原因是i和i'关于C对称,整体在R和L范围内,R和L也是关于C对称,传递得到。O(1) +- 情况1:i'的回文区域在L和R的内部。i不需要再求回文串,i的回文串的大小等于pArr[]中i'位置的长度。原因是i和i'关于C对称,整体在R和L范围内,R和L也是关于C对称,传递得到。O(1) -- 情况2:i'的回文区域的左边界在L的左侧。i的回文半径就是i位置到R位置的长度。原因是,L以i'为堆成的L',R以i为堆成的R'一定在L到R的范围内。且L'到L和R'到R互为回文。所以i区域的回文区域的回文半径至少为i到R的距离。由于以C为对称,得到区域为L到R,L不等于R,此处画图根据传递得到i的回文半径就是i位置到R位置的长度。O(1) +- 情况2:i'的回文区域的左边界在L的左侧。i的回文半径就是i位置到R位置的长度。原因是,L以i'为对称的L',R以i为堆成的R'一定在L到R的范围内。且L'到L和R'到R互为回文。所以i区域的回文区域的回文半径至少为i到R的距离。由于以C为对称,得到区域为L到R,L不等于R,此处画图根据传递得到i的回文半径就是i位置到R位置的长度。O(1) -- 情况3:i'的回文区域的左边界和L相等。i'的右区域一定不会再R的右侧。根据情况2,R以i堆成的R'。R和R'确定是回文,需要验证R下一个位置和R'前一个位置是否回文,这里也可以省掉R'到R之间的验证。O(N) +- 情况3:i'的回文区域的左边界和L相等。i'的右区域一定不会再R的右侧。根据情况2,R以i对称的R'。R和R'确定是回文,需要验证R下一个位置和R'前一个位置是否回文,这里也可以省掉R'到R之间的验证。O(N) 经过以上的情况,整体O(N)复杂度 +```Go +package main -Code: - -```Java -public class Code01_Manacher { +import ( + "fmt" + "math" +) - 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++) { - // R是第一个违规的位置,i>= R就表示i在R外 - // i位置扩出来的答案,i位置扩的区域,至少是多大。 - // 2 * C - 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; - } - } - //i的右边界有没有刷新之前的最右边界。R刷新C要跟着刷新 - if (i + pArr[i] > R) { - R = i + pArr[i]; - C = i; - } - max = Math.max(max, pArr[i]); - } - return max - 1; +// manacher 给定一个字符串,求该字符串的最长回文子串的大小 +func manacher(s string) int { + if len(s) == 0 { + return 0 } - 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++]; + // "12132" -> "#1#2#1#3#2#" + str := manacherString(s) + // 回文半径的大小 + pArr := make([]int, len(str)) + C := -1 + // 算法流程中,R代表最右的扩成功的位置。coding:最右的扩成功位置的,再下一个位置,即失败的位置 + R := -1 + max := math.MinInt + for i := 0; i < len(str); i++ { + // R是第一个违规的位置,i>= R就表示i在R外 + // i位置扩出来的答案,i位置扩的区域,至少是多大。 + // 2 * C - i 就是i的对称点。 + // 得到各种情况下无需验的区域 + if R > i { + pArr[i] = int(math.Min(float64(pArr[2 * C - i]), float64(R - i))) + } else { + pArr[i] = 1 } - return res; - } - // for test - public static int right(String s) { - if (s == null || s.length() == 0) { - return 0; - } - char[] str = manacherString(s); - int max = 0; - for (int i = 0; i < str.length; i++) { - int L = i - 1; - int R = i + 1; - while (L >= 0 && R < str.length && str[L] == str[R]) { - L--; - R++; + // 右侧不越界,且左侧不越界,检查需不需要扩 + for i + pArr[i] < len(str) && i - pArr[i] > -1 { + if str[i + pArr[i]] == str[i - pArr[i]] { + pArr[i]++ + } else { + break } - max = Math.max(max, R - L - 1); } - return max / 2; - } - // for test - public static String getRandomString(int possibilities, int size) { - char[] ans = new char[(int) (Math.random() * size) + 1]; - for (int i = 0; i < ans.length; i++) { - ans[i] = (char) ((int) (Math.random() * possibilities) + 'a'); + //i的右边界有没有刷新之前的最右边界。R刷新C要跟着刷新 + if i + pArr[i] > R { + R = i + pArr[i] + C = i } - return String.valueOf(ans); + max = int(math.Max(float64(max), float64(pArr[i]))) } - public static void main(String[] args) { - int possibilities = 5; - int strSize = 20; - int testTimes = 5000000; - System.out.println("test begin"); - for (int i = 0; i < testTimes; i++) { - String str = getRandomString(possibilities, strSize); - if (manacher(str) != right(str)) { - System.out.println("Oops!"); - } + return max - 1 +} + +func manacherString(str string) []byte { + charArr := []byte(str) + res := make([]byte, len(str) * 2 + 1) + index := 0 + for i := 0; i != len(res); i++ { + if (i & 1) == 0 { // 奇数位填充'#' + res[i] = '#' + } else { + res[i] = charArr[index] + index++ } - System.out.println("test finish"); } + return res +} +func main() { + s := "abc12321ef" + fmt.Println(manacher(s)) // 5 } ``` @@ -181,58 +159,78 @@ public class Code01_Manacher { > 解题思路:转化为必须包含最后一个字符的最长回文串多长?例如,"abc12321",以最后一个1的最长回文串为"12321",那么最少需要添加"cba"3个字符 -```Java -public class Code02_AddShortestEnd { +```Go +package main + +import ( + "fmt" + "math" +) - public static String shortestEnd(String s) { - if (s == null || s.length() == 0) { - return null; +func shortestEnd(s string) string { + if len(s) == 0 { + return "" + } + + str := manacherString(s) + pArr := make([]int, len(str)) + + C := -1 + R := -1 + maxContainsEnd := -1 + + for i := 0; i != len(str); i++ { + if R > i { + pArr[i] = int(math.Min(float64(pArr[2 * C - i]), float64(R - i))) + } else { + pArr[i] = 1 } - char[] str = manacherString(s); - int[] pArr = new int[str.length]; - int C = -1; - int R = -1; - int maxContainsEnd = -1; - for (int i = 0; i != str.length; 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; - } - if (R == str.length) { - maxContainsEnd = pArr[i]; - break; + + for i + pArr[i] < len(str) && i - pArr[i] > -1 { + if str[i + pArr[i]] == str[i - pArr[i]] { + pArr[i]++ + } else { + break } } - char[] res = new char[s.length() - maxContainsEnd + 1]; - for (int i = 0; i < res.length; i++) { - res[res.length - 1 - i] = str[i * 2 + 1]; + + if i + pArr[i] > R { + R = i + pArr[i] + C = i + } + + if R == len(str) { + maxContainsEnd = pArr[i] + break } - return String.valueOf(res); } - 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; + res := make([]byte, len(s) - maxContainsEnd + 1) + for i := 0; i < len(res); i++ { + res[len(res) - 1 -i] = str[i * 2 + 1] } - public static void main(String[] args) { - String str1 = "abcd123321"; - System.out.println(shortestEnd(str1)); + return string(res) +} + +func manacherString(str string) []byte { + charArr := []byte(str) + res := make([]byte, len(str) * 2 + 1) + index := 0 + for i := 0; i != len(res); i++ { + if (i & 1) == 0 { // 奇数位填充'#' + res[i] = '#' + } else { + res[i] = charArr[index] + index++ + } } + return res +} +func main() { + s := "abcd123321" + fmt.Println(shortestEnd(s)) // dcba => abcd123321dcba } ``` From 5ca3dbb7fb0da0bcea924319a342d9e05c07d00d Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Tue, 29 Mar 2022 21:21:33 +0800 Subject: [PATCH 03/18] morris refactor --- ...\200\213Morris\351\201\215\345\216\206.md" | 529 ++++++++---------- 1 file changed, 227 insertions(+), 302 deletions(-) diff --git "a/17-\343\200\212\350\277\233\351\230\266\343\200\213Morris\351\201\215\345\216\206.md" "b/17-\343\200\212\350\277\233\351\230\266\343\200\213Morris\351\201\215\345\216\206.md" index 12455a8..74f376c 100644 --- "a/17-\343\200\212\350\277\233\351\230\266\343\200\213Morris\351\201\215\345\216\206.md" +++ "b/17-\343\200\212\350\277\233\351\230\266\343\200\213Morris\351\201\215\345\216\206.md" @@ -45,243 +45,199 @@ cur来到节点的时间复杂度为O(N),每个cur遍历左树最右边界的代 -```Java -public class Code01_MorrisTraversal { +```Go +package main - public static class Node { - public int value; - Node left; - Node right; +import ( + "fmt" +) - public Node(int data) { - this.value = data; - } - } +type Node struct { + Left *Node + Right *Node + Val int +} - // morris遍历 - public static void morris(Node head) { - if (head == null) { - return; - } - Node cur = head; - Node mostRight = null; - while (cur != null) { - // cur有没有左树 - mostRight = cur.left; - if (mostRight != null) { // 有左树的情况下 - // 找到cur左树上,真实的最右 - while (mostRight.right != null && mostRight.right != cur) { - mostRight = mostRight.right; - } - // 从while中出来,mostRight一定是cur左树上的最右节点 - // mostRight如果等于null,说明第一次来到自己 - if (mostRight.right == null) { - mostRight.right = cur; - cur = cur.left; - continue; - // 否则第二次来到自己,意味着mostRight.right = cur - } else { // mostRight.right != null -> mostRight.right == cur - mostRight.right = null; - } - } - cur = cur.right; - } +// morris遍历二叉树,实现时间复杂度O(N), 空间复杂度O(1)。正常的递归遍历,时间复杂度O(N),空间复杂度O(N) +func morris(head *Node) { + if head == nil { + return } - // Morris中序遍历 - public static void morrisIn(Node head) { - if (head == null) { - return; - } - Node cur = head; - Node mostRight = null; - while (cur != null) { - mostRight = cur.left; - if (mostRight != null) { - while (mostRight.right != null && mostRight.right != cur) { - mostRight = mostRight.right; - } - if (mostRight.right == null) { - mostRight.right = cur; - cur = cur.left; - continue; - } else { - mostRight.right = null; - } + cur := head + var mostRight *Node + for cur != nil { + // cur有没有左树 + mostRight = cur.Left + if mostRight != nil { // 有左树的情况下 + // 找到cur左树上,真实的最右 + for mostRight.Right != nil && mostRight.Right != cur { + mostRight = mostRight.Right } - System.out.print(cur.value + " "); - cur = cur.right; - } - System.out.println(); - } - // Morris先序遍历 - public static void morrisPre(Node head) { - if (head == null) { - return; - } - // cur - Node cur1 = head; - // mostRight - 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; - } + // 从while中出来,mostRight一定是cur左树上的最右节点 + // mostRight如果等于null,说明第一次来到自己 + if mostRight.Right == nil { + mostRight.Right = cur + cur = cur.Left + continue } else { - System.out.print(cur1.value + " "); + // 否则第二次来到自己,意味着mostRight.right = cur + // mostRight.right != null -> mostRight.right == cur + mostRight.Right = nil } - cur1 = cur1.right; } - System.out.println(); + cur = cur.Right + } +} + +// morris 先序遍历二叉树 +func morrisPre(head *Node) { + if head == nil { + return } + // cur + cur1 := head + // mostRight + var cur2 *Node - // Morris后序遍历 - public static void morrisPos(Node head) { - if (head == null) { - return; - } - Node cur = head; - Node mostRight = null; - while (cur != null) { - mostRight = cur.left; - if (mostRight != null) { - while (mostRight.right != null && mostRight.right != cur) { - mostRight = mostRight.right; - } - if (mostRight.right == null) { - mostRight.right = cur; - cur = cur.left; - continue; - // 回到自己两次,且第二次回到自己的时候是打印时机 - } else { - mostRight.right = null; - // 翻转右边界链表,打印 - printEdge(cur.left); - } + for cur1 != nil { + cur2 = cur1.Left + if cur2 != nil { + for cur2.Right != nil && cur2.Right != cur1 { + cur2 = cur2.Right } - cur = cur.right; + if cur2.Right == nil { + cur2.Right = cur1 + fmt.Print(fmt.Sprintf("%d%s", cur1.Val, " ")) + cur1 = cur1.Left + continue + } else { + cur2.Right = nil + } + } else { + fmt.Print(fmt.Sprintf("%d%s", cur1.Val, " ")) } - // while结束的时候,整颗树的右边界同样的翻转打印一次 - printEdge(head); - System.out.println(); + cur1 = cur1.Right } + fmt.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); +// morris 中序遍历 +func morrisIn(head *Node) { + if head == nil { + return } - 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; + cur := head + var mostRight *Node + for cur != nil { + mostRight = cur.Left + if mostRight != nil { + for mostRight.Right != nil && mostRight.Right != cur { + mostRight = mostRight.Right + } + if mostRight.Right == nil { + mostRight.Right = cur + cur = cur.Left + continue + } else { + mostRight.Right = nil + } } - return pre; + fmt.Print(fmt.Sprintf("%d%s", cur.Val, " ")) + cur = cur.Right } + fmt.Println() +} - // for test -- print tree - public static void printTree(Node head) { - System.out.println("Binary Tree:"); - printInOrder(head, 0, "H", 17); - System.out.println(); +// morris 后序遍历 +func morrisPos(head *Node) { + if head == nil { + return } - public static void printInOrder(Node head, int height, String to, int len) { - if (head == null) { - return; + cur := head + var mostRight *Node + for cur != nil { + mostRight = cur.Left + if mostRight != nil { + for mostRight.Right != nil && mostRight.Right != cur { + mostRight = mostRight.Right + } + if mostRight.Right == nil { + mostRight.Right = cur + cur = cur.Left + continue + } else { // 回到自己两次,且第二次回到自己的时候是打印时机 + mostRight.Right = nil + // 翻转右边界链表,打印 + printEdge(cur.Left) + } } - printInOrder(head.right, height + 1, "v", len); - String val = to + head.value + to; - int lenM = val.length(); - int lenL = (len - lenM) / 2; - int lenR = len - lenM - lenL; - val = getSpace(lenL) + val + getSpace(lenR); - System.out.println(getSpace(height * len) + val); - printInOrder(head.left, height + 1, "^", len); + cur = cur.Right } + // while结束的时候,整颗树的右边界同样的翻转打印一次 + printEdge(head) + fmt.Println() +} - public static String getSpace(int num) { - String space = " "; - StringBuffer buf = new StringBuffer(""); - for (int i = 0; i < num; i++) { - buf.append(space); - } - return buf.toString(); +func printEdge(head *Node) { + tali := reverseEdge(head) + cur := tali + for cur != nil { + fmt.Print(fmt.Sprintf("%d%s", cur.Val, " ")) + cur = cur.Right } + reverseEdge(tali) +} +func reverseEdge(from *Node) *Node { + var pre *Node + var next *Node + for from != nil { + next = from.Right + from.Right = pre + pre = from + from = next + } + return pre +} - - // 在Morris遍历的基础上,判断一颗树是不是一颗搜索二叉树 - // 搜索二叉树是左比自己小,右比自己大 - // 一颗树中序遍历,值一直在递增,就是搜索二叉树 - public static boolean isBST(Node head) { - if (head == null) { - return true; - } - Node cur = head; - Node mostRight = null; - Integer pre = null; - boolean ans = true; - while (cur != null) { - mostRight = cur.left; - if (mostRight != null) { - while (mostRight.right != null && mostRight.right != cur) { - mostRight = mostRight.right; - } - if (mostRight.right == null) { - mostRight.right = cur; - cur = cur.left; - continue; - } else { - mostRight.right = null; - } +// 在Morris遍历的基础上,判断一颗树是不是一颗搜索二叉树 +// 搜索二叉树是左比自己小,右比自己大 +// 一颗树中序遍历,值一直在递增,就是搜索二叉树 +func isBST(head *Node) bool { + if head == nil { + return true + } + + cur := head + var mostRight *Node + var pre int + var ans bool + for cur != nil { + mostRight = cur.Left + if mostRight != nil { + for mostRight.Right != nil && mostRight.Right != cur { + mostRight = mostRight.Right } - if (pre != null && pre >= cur.value) { - ans = false; + if mostRight.Right == nil { + mostRight.Right = cur + cur = cur.Left + continue + } else { + mostRight.Right = nil } - pre = cur.value; - cur = cur.right; } - return ans; - } - - public static void main(String[] args) { - Node head = new Node(4); - head.left = new Node(2); - head.right = new Node(6); - head.left.left = new Node(1); - head.left.right = new Node(3); - head.right.left = new Node(5); - head.right.right = new Node(7); - printTree(head); - morrisIn(head); - morrisPre(head); - morrisPos(head); - printTree(head); - + if pre >= cur.Val { + ans = false + } + pre = cur.Val + cur = cur.Right } - + return ans } ``` @@ -294,122 +250,91 @@ public class Code01_MorrisTraversal { > Morris遍历求解,每到达一个cur的时候,记录高度。每到达一个cur的时候判断cur是否为叶子节点,更新全局最小值。最后看一下最后一个节点的高度和全局最小高度对比,取最小高度 -```Java -public class Code05_MinHeight { +```Go +package main - public static class Node { - public int val; - public Node left; - public Node right; +import "math" - public Node(int x) { - val = x; - } - } +type Node struct { + Left *Node + Right *Node + Val int +} - // 解法1 运用二叉树的递归 - public static int minHeight1(Node head) { - if (head == null) { - return 0; - } - return p(head); +// 求二叉树最小高度;解法1 运用二叉树的递归 +func minHeight1(head *Node) int { + if head == nil { + return 0 } + return p(head) +} - public static int p(Node x) { - if (x.left == null && x.right == null) { - return 1; - } - // 左右子树起码有一个不为空 - int leftH = Integer.MAX_VALUE; - if (x.left != null) { - leftH = p(x.left); - } - int rightH = Integer.MAX_VALUE; - if (x.right != null) { - rightH = p(x.right); - } - return 1 + Math.min(leftH, rightH); +func p(x *Node) int { + if x.Left == nil && x.Right == nil { + return 1 } - // 解法2 根据morris遍历改写 - public static int minHeight2(Node head) { - if (head == null) { - return 0; - } - Node cur = head; - Node mostRight = null; - int curLevel = 0; - int minHeight = Integer.MAX_VALUE; - while (cur != null) { - mostRight = cur.left; - if (mostRight != null) { - int rightBoardSize = 1; - while (mostRight.right != null && mostRight.right != cur) { - rightBoardSize++; - mostRight = mostRight.right; - } - if (mostRight.right == null) { // 第一次到达 - curLevel++; - mostRight.right = cur; - cur = cur.left; - continue; - } else { // 第二次到达 - if (mostRight.left == null) { - minHeight = Math.min(minHeight, curLevel); - } - curLevel -= rightBoardSize; - mostRight.right = null; - } - } else { // 只有一次到达 - curLevel++; - } - cur = cur.right; - } - int finalRight = 1; - cur = head; - while (cur.right != null) { - finalRight++; - cur = cur.right; - } - if (cur.left == null && cur.right == null) { - minHeight = Math.min(minHeight, finalRight); - } - return minHeight; + // 左右子树起码有一个不为空 + leftH := math.MaxInt + if x.Left != nil { + leftH = p(x.Left) } - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); + rightH := math.MaxInt + if x.Right != nil { + rightH = p(x.Right) } - // for test - public static Node generate(int level, int maxLevel, int maxValue) { - if (level > maxLevel || Math.random() < 0.5) { - return null; - } - Node head = new Node((int) (Math.random() * maxValue)); - head.left = generate(level + 1, maxLevel, maxValue); - head.right = generate(level + 1, maxLevel, maxValue); - return head; + return 1 + int(math.Min(float64(leftH), float64(rightH))) +} + +// 解法2 根据morris遍历改写 +func minHeight2(head *Node) int { + if head == nil { + return 0 } - public static void main(String[] args) { - int treeLevel = 7; - int nodeMaxValue = 5; - int testTimes = 100000; - System.out.println("test begin"); - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(treeLevel, nodeMaxValue); - int ans1 = minHeight1(head); - int ans2 = minHeight2(head); - if (ans1 != ans2) { - System.out.println("Oops!"); + cur := head + var mostRight *Node + curLevel := 0 + minnHeight := math.MaxInt + for cur != nil { + mostRight = cur.Left + if mostRight != nil { + rightBoardSize := 1 + for mostRight.Right != nil && mostRight.Right != cur { + rightBoardSize++ + mostRight = mostRight.Right } - } - System.out.println("test finish!"); + if mostRight.Right == nil { // 第一次到达 + curLevel++ + mostRight.Right = cur + cur = cur.Left + continue + } else { // 第二次到达 + if mostRight.Left == nil { + minnHeight = int(math.Min(float64(minnHeight), float64(minnHeight))) + } + curLevel -= rightBoardSize + mostRight.Right = nil + } + } else { // 只有一次到达 + curLevel++ + } + cur = cur.Right } + finalRight := 1 + cur = head // 回到头结点 + for cur.Right != nil { + finalRight++ + cur = cur.Right + } + if cur.Left == nil && cur.Right == nil { + minnHeight = int(math.Min(float64(minnHeight), float64(finalRight))) + } + return minnHeight } ``` From 2fef3805dff8444a2e429d794030565ac621d6b0 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Wed, 30 Mar 2022 19:59:55 +0800 Subject: [PATCH 04/18] fix: stack Pop --- ...5\205\263\347\256\227\346\263\225\344\273\213\347\273\215.md" | 1 + 1 file changed, 1 insertion(+) diff --git "a/10-\345\271\266\346\237\245\351\233\206\343\200\201\345\233\276\347\233\270\345\205\263\347\256\227\346\263\225\344\273\213\347\273\215.md" "b/10-\345\271\266\346\237\245\351\233\206\343\200\201\345\233\276\347\233\270\345\205\263\347\256\227\346\263\225\344\273\213\347\273\215.md" index 37962d4..5fba2d7 100644 --- "a/10-\345\271\266\346\237\245\351\233\206\343\200\201\345\233\276\347\233\270\345\205\263\347\256\227\346\263\225\344\273\213\347\273\215.md" +++ "b/10-\345\271\266\346\237\245\351\233\206\343\200\201\345\233\276\347\233\270\345\205\263\347\256\227\346\263\225\344\273\213\347\273\215.md" @@ -61,6 +61,7 @@ func (set *UnionSet) FindFather(cur *Node) *Node { for len(path) != 0 { for i := len(path) - 1; i >= 0; i-- { set.Parents[path[i]] = cur + path = path[:len(path) - 1] // 模拟栈的弹出 } } return cur From 0894ecf72a80c446d9f7e7569cfffd87b0202d22 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Mon, 4 Apr 2022 14:57:52 +0800 Subject: [PATCH 05/18] interval tree --- ...346\256\265\346\240\221(interval-tree).md" | 704 ++++++++---------- 1 file changed, 313 insertions(+), 391 deletions(-) diff --git "a/18-\343\200\212\350\277\233\351\230\266\343\200\213\347\272\277\346\256\265\346\240\221(interval-tree).md" "b/18-\343\200\212\350\277\233\351\230\266\343\200\213\347\272\277\346\256\265\346\240\221(interval-tree).md" index 2876d72..b3de81a 100644 --- "a/18-\343\200\212\350\277\233\351\230\266\343\200\213\347\272\277\346\256\265\346\240\221(interval-tree).md" +++ "b/18-\343\200\212\350\277\233\351\230\266\343\200\213\347\272\277\346\256\265\346\240\221(interval-tree).md" @@ -6,16 +6,20 @@ 线段树结构提供三个主要的方法, 假设大小为N的数组,以下三个方法,均要达到O(logN) : -```Java -// L到R范围的数,每个数加上V -void add(int L, int R, int V, int[] arr); - -// L到R范围的数,每个数都更新成V -void update(int L, int R, int V, int[] arr); - -// L到R范围的数,累加和返回 -int getSum(int L, int R, int[] arr); - +```Go +type SegmentTreeInterface interface { + // Add L到R范围的数,每个数加上V + // Add(L, R, V int, arr []int) + Add(L, R, C, l, r, rt int) + + // Update L到R范围的数,每个数都更新成V + // Update(L, R, V int, arr []int) + Update(L, R, C, l, r, rt int) + + // GetSum L到R范围的数,累加和返回 + // GetSum(L, R int, arr []int) + GetSum(L, R, l, r, rt int) int +} ``` @@ -78,295 +82,277 @@ graph TD 对于update操作,如果update操作经过的信息节点上存在懒任务,那么该次update操作会取消该节点的lazy,无需下发,因为下发了也会给update覆盖掉; -```Java -public class Code01_SegmentTree { - - public static class SegmentTree { - // arr[]为原序列的信息从0开始,但在arr里是从1开始的 - // sum[]模拟线段树维护区间和 - // lazy[]为累加懒惰标记 - // change[]为更新的值 - // update[]为更新慵懒标记 - private int MAXN; - private int[] arr; - // 4*arr.length() - private int[] sum; - // 4*arr.length() - private int[] lazy; - // 4*arr.length() - private int[] change; - // 4*arr.length() - private boolean[] update; - - // 根据int[] origin来初始化我们的线段树结构 - public SegmentTree(int[] origin) { - MAXN = origin.length + 1; - arr = new int[MAXN]; // arr[0] 不用 从1开始使用 - for (int i = 1; i < MAXN; i++) { - arr[i] = origin[i - 1]; - } - // sum数组开辟的大小是原始数组的4倍 - sum = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围的累加和信息 - - lazy = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围沒有往下傳遞的纍加任務 - change = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围有没有更新操作的任务 - update = new boolean[MAXN << 2]; // 用来支持脑补概念中,某一个范围更新任务,更新成了什么 - } - - // 汇总当前位置rt的信息,为左孩子信息加上右孩子信息 - private void pushUp(int rt) { - sum[rt] = sum[rt << 1] + sum[rt << 1 | 1]; - } - - // 之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围 - // 分发策略是什么 - // ln表示左子树元素结点个数,rn表示右子树结点个数 - private void pushDown(int rt, int ln, int rn) { - // 首先检查父亲范围上有没有懒更新操作 - if (update[rt]) { - // 父范围有懒更新操作,左右子范围就有懒更新操作 - update[rt << 1] = true; - update[rt << 1 | 1] = true; - // 左右子范围的change以父亲分发的为准 - change[rt << 1] = change[rt]; - change[rt << 1 | 1] = change[rt]; - // 左右子范围的懒任务全部清空 - lazy[rt << 1] = 0; - lazy[rt << 1 | 1] = 0; - // 左右子范围的累加和全部变为当前父节点下发的change乘以左右孩子的范围个数 - sum[rt << 1] = change[rt] * ln; - sum[rt << 1 | 1] = change[rt] * rn; - // 父范围的更新任务被分发到左右子范围,当前父范围的更新任务改为false - update[rt] = false; - } - - // 如果上面的if也进入,该if也进入,表示之前的最晚懒住的更新到现在还没有发生过新的更新使之下发,却来了个add任务 - // 所以该节点即懒住了更新任务,又懒住一个add任务,接着又来了一个update任务,所以更新要先下发到子范围,接着要把当前的add任务下发下去 - // 如果当前节点的懒信息不为空。 - if (lazy[rt] != 0) { - // 下发给左孩子 - lazy[rt << 1] += lazy[rt]; - sum[rt << 1] += lazy[rt] * ln; - // 下发给右孩子 - lazy[rt << 1 | 1] += lazy[rt]; - sum[rt << 1 | 1] += lazy[rt] * rn; - // 清空当前节点的懒任务信息 - lazy[rt] = 0; - } - } - - // 在初始化阶段,先把sum数组,填好 - // 在arr[l~r]范围上,去build,1~N, - // rt : 这个范围在sum中的下标 - public void build(int l, int r, int rt) { - if (l == r) { - sum[rt] = arr[l]; - return; - } - // 得到l到r的中间位置 - int mid = (l + r) >> 1; - // l到r左侧,填充到sum数组rt下标的2倍的位置,因为在数组中当前节点和左孩子的关系得到 - // 递归rt左区间 - build(l, mid, rt << 1); - // 右侧,填充到2*rt+1的位置 - // 递归rt右区间 - build(mid + 1, r, rt << 1 | 1); - pushUp(rt); - } - - // 更新操作 - public void update(int L, int R, int C, int l, int r, int rt) { - // 如果更新任务彻底覆盖当前边界 - if (L <= l && r <= R) { - // 当前位置的update标记为true - update[rt] = true; - // 当前位置需要改变为C, update和change搭配使用 - change[rt] = C; - // 当前节点的累加和信息,被C * (r - l + 1)覆盖掉 - sum[rt] = C * (r - l + 1); - // 清空之前存在该节点的懒任务 - lazy[rt] = 0; - return; - } - // 当前任务躲不掉,无法懒更新,要往下发 - int mid = (l + r) >> 1; - // 之前的,所有懒更新,从父范围,发给左右两个子范围 - pushDown(rt, mid - l + 1, r - mid); - // 更新任务发给左孩子 - if (L <= mid) { - update(L, R, C, l, mid, rt << 1); - } - // 更新任务发给右孩子 - if (R > mid) { - update(L, R, C, mid + 1, r, rt << 1 | 1); - } - pushUp(rt); - } - - // L..R -> 任务范围 ,所有的值累加上C - // l,r -> 表达的范围 - // rt 去哪找l,r范围上的信息 - public void add( - int L, int R, int C, - int l, int r, - int rt) { - // 任务的范围彻底覆盖了,当前表达的范围,懒住 - if (L <= l && r <= R) { - // 当前位置的累加和加上C * (r - l + 1),等同于下边节点都加上C,由于被懒住,下面节点并没有真正意思上add一个C - sum[rt] += C * (r - l + 1); - // 之前懒住的信息,例如之前该节点加上3,又来一个加上7的任务,那么此时lazt[rt]==10 - lazy[rt] += C; - return; - } - // 任务并没有把l...r全包住 - // 要把当前任务往下发 - // 任务 L, R 没有把本身表达范围 l,r 彻底包住 - int mid = (l + r) >> 1; // l..mid (rt << 1) mid+1...r(rt << 1 | 1) - // 下发之前该节点所有攒的懒任务到孩子节点 - pushDown(rt, mid - l + 1, r - mid); - // 左孩子是否需要接到任务 - if (L <= mid) { - add(L, R, C, l, mid, rt << 1); - } - // 右孩子是否需要接到任务 - if (R > mid) { - add(L, R, C, mid + 1, r, rt << 1 | 1); - } - // 左右孩子做完任务后,我更新我的sum信息 - pushUp(rt); - } - - // 1~6 累加和是多少? 1~8 rt - public long query(int L, int R, int l, int r, int rt) { - // 累加任务覆盖当前节点范围,返回当前节点范围的累加和 - if (L <= l && r <= R) { - return sum[rt]; - } - // 没覆盖当前节点的范围,汇总左右子范围的累加和,汇总给到当前节点 - int mid = (l + r) >> 1; - pushDown(rt, mid - l + 1, r - mid); - long ans = 0; - if (L <= mid) { - ans += query(L, R, l, mid, rt << 1); - } - if (R > mid) { - ans += query(L, R, mid + 1, r, rt << 1 | 1); - } - return ans; - } +```Go +package main + +import "fmt" + +type SegmentTreeInterface interface { + // Add L到R范围的数,每个数加上V + // Add(L, R, V int, arr []int) + Add(L, R, C, l, r, rt int) + + // Update L到R范围的数,每个数都更新成V + // Update(L, R, V int, arr []int) + Update(L, R, C, l, r, rt int) + // GetSum L到R范围的数,累加和返回 + // GetSum(L, R int, arr []int) + GetSum(L, R, l, r, rt int) int +} + +// SegmentTree 线段树 +type SegmentTree struct { + // arr[]为原序列的信息从0开始,但在arr里是从1开始的 + // sum[]模拟线段树维护区间和 + // lazy[]为累加懒惰标记 + // change[]为更新的值 + // update[]为更新慵懒标记 + maxN int + arr []int + // 4*len(arr) + sum []int + // 4*len(arr) + lazy []int + // 4*len(arr) + change []int + // 4*len(arr) + update []bool +} + +// InitSegmentTree 初始化一个线段树。根据int[] origin来初始化线段树结构 +func InitSegmentTree(origin []int) *SegmentTree { + sgTree := SegmentTree{} + MaxN := len(origin) + 1 + arr := make([]int, MaxN) // arr[0] 不用 从1开始使用 + for i := 1; i < MaxN; i++ { + arr[i] = origin[i - 1] } - // 暴力解法,用来做对数器 - public static class Right { - public int[] arr; - - public Right(int[] origin) { - arr = new int[origin.length + 1]; - // 做一层拷贝,arr[0]位置废弃不用,下标从1开始 - for (int i = 0; i < origin.length; i++) { - arr[i + 1] = origin[i]; - } - } - - public void update(int L, int R, int C) { - for (int i = L; i <= R; i++) { - arr[i] = C; - } - } - - public void add(int L, int R, int C) { - for (int i = L; i <= R; i++) { - arr[i] += C; - } - } - - public long query(int L, int R) { - long ans = 0; - for (int i = L; i <= R; i++) { - ans += arr[i]; - } - return ans; - } + // sum数组开辟的大小是原始数组的4倍 + sum := make([]int, MaxN << 2) // 用来支持脑补概念中,某一个范围的累加和信息 + lazy := make([]int, MaxN << 2) // 用来支持脑补概念中,某一个范围沒有往下傳遞的纍加任務 + change := make([]int, MaxN << 2) // 用来支持脑补概念中,某一个范围有没有更新操作的任务 + update := make([]bool, MaxN << 2) // 用来支持脑补概念中,某一个范围更新任务,更新成了什么 + + sgTree.maxN = MaxN + sgTree.arr = arr + sgTree.sum = sum + sgTree.lazy = lazy + sgTree.change = change + sgTree.update = update + return &sgTree +} +// PushUp 汇总线段树当前位置rt的信息,为左孩子信息加上右孩子信息 +func (sgTree *SegmentTree) PushUp(rt int) { + sgTree.sum[rt] = sgTree.sum[rt << 1] + sgTree.sum[rt << 1 | 1] +} + +// PushDown 线段树之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围 +// 分发策略是什么 +// ln表示左子树元素结点个数,rn表示右子树结点个数 +func (sgTree *SegmentTree) PushDown(rt, ln, rn int) { + // 首先检查父亲范围上有没有懒更新操作 + if sgTree.update[rt] { + // 父范围有懒更新操作,左右子范围就有懒更新操作 + sgTree.update[rt << 1] = true + sgTree.update[rt << 1 | 1] = true + // 左右子范围的change以父亲分发的为准 + sgTree.change[rt << 1] = sgTree.change[rt] + sgTree.change[rt << 1 | 1] = sgTree.change[rt] + // 左右子范围的懒任务全部清空 + sgTree.lazy[rt << 1] = 0 + sgTree.lazy[rt << 1 | 1] = 0 + // 左右子范围的累加和全部变为当前父节点下发的change乘以左右孩子的范围个数 + sgTree.sum[rt << 1] = sgTree.change[rt] * ln + sgTree.sum[rt << 1 | 1] = sgTree.change[rt] * rn + // 父范围的更新任务被分发到左右子范围,当前父范围的更新任务改为false + sgTree.update[rt] = false + } + + // 如果上面的if也进入,该if也进入,表示之前的最晚懒住的更新到现在还没有发生过新的更新使之下发,却来了个add任务 + // 所以该节点即懒住了更新任务,又懒住一个add任务,接着又来了一个update任务,所以更新要先下发到子范围,接着要把当前的add任务下发下去 + // 如果当前节点的懒信息不为空。 + if sgTree.lazy[rt] != 0 { + // 下发给左孩子 + sgTree.lazy[rt << 1] += sgTree.lazy[rt] + sgTree.sum[rt << 1] += sgTree.lazy[rt] * ln + // 下发给右孩子 + sgTree.lazy[rt << 1 | 1] += sgTree.lazy[rt] + sgTree.sum[rt << 1 | 1] += sgTree.lazy[rt] * rn + // 清空当前节点的懒任务信息 + sgTree.lazy[rt] = 0 } +} - public static int[] genarateRandomArray(int len, int max) { - int size = (int) (Math.random() * len) + 1; - int[] origin = new int[size]; - for (int i = 0; i < size; i++) { - origin[i] = (int) (Math.random() * max) - (int) (Math.random() * max); - } - return origin; +// Build 在初始化阶段,先把sum数组,填好 +// 在arr[l~r]范围上,去build,1~N, +// rt : 这个范围在sum中的下标 +func (sgTree *SegmentTree) Build(l, r, rt int) { + if l == r { + sgTree.sum[rt] = sgTree.arr[l] + return } + // 得到l到r的中间位置 + mid := (l + r) >> 1 + // l到r左侧,填充到sum数组rt下标的2倍的位置,因为在数组中当前节点和左孩子的关系得到 + // 递归rt左区间 + sgTree.Build(l, mid, rt << 1) + // 右侧,填充到2*rt+1的位置 + // 递归rt右区间 + sgTree.Build(mid + 1, r, rt << 1 | 1) + sgTree.PushUp(rt) +} - public static boolean test() { - int len = 100; - int max = 1000; - int testTimes = 5000; - int addOrUpdateTimes = 1000; - int queryTimes = 500; - for (int i = 0; i < testTimes; i++) { - int[] origin = genarateRandomArray(len, max); - SegmentTree seg = new SegmentTree(origin); - int S = 1; - int N = origin.length; - int root = 1; - seg.build(S, N, root); - Right rig = new Right(origin); - for (int j = 0; j < addOrUpdateTimes; j++) { - int num1 = (int) (Math.random() * N) + 1; - int num2 = (int) (Math.random() * N) + 1; - int L = Math.min(num1, num2); - int R = Math.max(num1, num2); - int C = (int) (Math.random() * max) - (int) (Math.random() * max); - if (Math.random() < 0.5) { - seg.add(L, R, C, S, N, root); - rig.add(L, R, C); - } else { - seg.update(L, R, C, S, N, root); - rig.update(L, R, C); - } - } - for (int k = 0; k < queryTimes; k++) { - int num1 = (int) (Math.random() * N) + 1; - int num2 = (int) (Math.random() * N) + 1; - int L = Math.min(num1, num2); - int R = Math.max(num1, num2); - long ans1 = seg.query(L, R, S, N, root); - long ans2 = rig.query(L, R); - if (ans1 != ans2) { - return false; - } - } - } - return true; +// Update 线段树更新操作 +func (sgTree *SegmentTree) Update(L, R, C, l, r, rt int) { + // 如果更新任务彻底覆盖当前边界 + if L <= l && r <= R { + // 当前位置的update标记为true + sgTree.update[rt] = true + // 当前位置需要改变为C, update和change搭配使用 + sgTree.change[rt] = C + // 当前节点的累加和信息,被C * (r - l + 1)覆盖掉 + sgTree.sum[rt] = C * (r -l + 1) + // 清空之前存在该节点的懒任务 + sgTree.lazy[rt] = 0 + return + } + // 当前任务躲不掉,无法懒更新,要往下发 + mid := (l + r) >> 1 + // 之前的,所有懒更新,从父范围,发给左右两个子范围 + sgTree.PushDown(rt, mid - l + 1, r - mid) + // 更新任务发给左孩子 + if L <= mid { + sgTree.Update(L, R, C, l, mid, rt << 1) } + // 更新任务发给右孩子 + if R > mid { + sgTree.Update(L, R, C, mid + 1, r, rt << 1 | 1) + } + + sgTree.PushUp(rt) +} - public static void main(String[] args) { - int[] origin = { 2, 1, 1, 2, 3, 4, 5 }; - SegmentTree seg = new SegmentTree(origin); - int S = 1; // 整个区间的开始位置,规定从1开始,不从0开始 -> 固定 - int N = origin.length; // 整个区间的结束位置,规定能到N,不是N-1 -> 固定 - int root = 1; // 整棵树的头节点位置,规定是1,不是0 -> 固定 - int L = 2; // 操作区间的开始位置 -> 可变 - int R = 5; // 操作区间的结束位置 -> 可变 - int C = 4; // 要加的数字或者要更新的数字 -> 可变 - // 区间生成,必须在[S,N]整个范围上build - seg.build(S, N, root); - // 区间修改,可以改变L、R和C的值,其他值不可改变 - seg.add(L, R, C, S, N, root); - // 区间更新,可以改变L、R和C的值,其他值不可改变 - seg.update(L, R, C, S, N, root); - // 区间查询,可以改变L和R的值,其他值不可改变 - long sum = seg.query(L, R, S, N, root); - System.out.println(sum); - - System.out.println("对数器测试开始..."); - System.out.println("测试结果 : " + (test() ? "通过" : "未通过")); +// Add 线段树加值操作 +// L..R -> 任务范围 ,所有的值累加上C +// l,r -> 表达的范围 +// rt 去哪找l,r范围上的信息 +func (sgTree *SegmentTree) Add(L, R, C, l, r, rt int) { + // 任务的范围彻底覆盖了,当前表达的范围,懒住 + if L <= l && r <= R { + // 当前位置的累加和加上C * (r - l + 1),等同于下边节点都加上C,由于被懒住,下面节点并没有真正意思上add一个C + sgTree.sum[rt] += C * (r - l + 1) + // 之前懒住的信息,例如之前该节点加上3,又来一个加上7的任务,那么此时lazt[rt]==10 + sgTree.lazy[rt] += C + return + } + // 任务并没有把l...r全包住 + // 要把当前任务往下发 + // 任务 L, R 没有把本身表达范围 l,r 彻底包住 + mid := (l + r) >> 1 // l..mid (rt << 1) mid+1...r(rt << 1 | 1) + // 下发之前该节点所有攒的懒任务到孩子节点 + sgTree.PushDown(rt, mid - l + 1, r - mid) + // 左孩子是否需要接到任务 + if L <= mid { + sgTree.Add(L, R, C, l, mid, rt << 1) } + // 右孩子是否需要接到任务 + if R > mid { + sgTree.Add(L, R, C, mid + 1, r, rt << 1 | 1) + } + // 左右孩子做完任务后,我更新我的sum信息 + sgTree.PushUp(rt) +} + +// GetSum 1~6 累加和是多少? 1~8 rt +func (sgTree *SegmentTree) GetSum(L, R, l, r, rt int) int { + // 累加任务覆盖当前节点范围,返回当前节点范围的累加和 + if L <= l && r <= R { + return sgTree.sum[rt] + } + + // 没覆盖当前节点的范围,汇总左右子范围的累加和,汇总给到当前节点 + mid := (l + r) >> 1 + sgTree.PushDown(rt, mid - l + 1, r - mid) + ans := 0 + if L <= mid { + ans += sgTree.GetSum(L, R, l, mid, rt << 1) + } + if R > mid { + ans += sgTree.GetSum(L, R, mid + 1, r, rt << 1 | 1) + } + return ans +} + + +// ---------- // +// 线段树暴力解,用来做对数器 +// sgTree 模拟线段树结构 +type sgTree []int + +// BuildTestTree 构建测试线段树 +func BuildTestTree(origin []int) *sgTree { + arr := make([]int, len(origin) + 1) + // 做一层拷贝,arr[0]位置废弃不用,下标从1开始 + for i := 0; i < len(origin); i++ { + arr[i + 1] = origin[i] + } + sg := sgTree{} + sg = arr + return &sg +} + +func (sgt *sgTree) Update(L, R, C int) { + for i := L; i <= R; i++ { + (*sgt)[i] = C + } +} + +func (sgt *sgTree) Add(L, R, C int) { + for i := L; i <= R; i++ { + (*sgt)[i] += C + } +} + +func (sgt *sgTree) GetSum(L, R int) int { + ans := 0 + for i := L; i <= R; i++ { + ans += (*sgt)[i] + } + return ans +} + +func main() { + origin := []int{2, 1, 1, 2, 3, 4, 5} + // 构建一个线段树 + sg := InitSegmentTree(origin) + sgTest := BuildTestTree(origin) + + + S := 1 // 整个区间的开始位置,规定从1开始,不从0开始 -> 固定 + N := len(origin) // 整个区间的结束位置,规定能到N,不是N-1 -> 固定 + root := 1 // 整棵树的头节点位置,规定是1,不是0 -> 固定 + L := 2 // 操作区间的开始位置 -> 可变 + R := 5 // 操作区间的结束位置 -> 可变 + C := 4 // 要加的数字或者要更新的数字 -> 可变 + // 区间生成,必须在[S,N]整个范围上build + sg.Build(S, N , root) + // 区间修改,可以改变L、R和C的值,其他值不可改变 + sg.Add(L, R, C, S, N, root) + // 区间更新,可以改变L、R和C的值,其他值不可改变 + sg.Update(L, R, C, S, N ,root) + // 区间查询,可以改变L和R的值,其他值不可改变 + sum := sg.GetSum(L, R, S, N, root) + fmt.Println(fmt.Sprintf("segmentTree: %d", sum)) + + sgTest.Add(L, R, C) + sgTest.Update(L, R, C) + testSum := sgTest.GetSum(L, R) + fmt.Println(fmt.Sprintf("segmentTreeTest: %d", testSum)) } ``` @@ -388,116 +374,52 @@ public class Code01_SegmentTree { > 线段树原结构,是收集范围累加和,本题是范围上收集最大高度当成收集的信息 -```Java -public class Code02_FallingSquares { - - // 0位置不用,从1开始 - public static class SegmentTree { - private int[] max; - private int[] change; - private boolean[] update; - - public SegmentTree(int size) { - int N = size + 1; - max = new int[N << 2]; - - change = new int[N << 2]; - update = new boolean[N << 2]; - } - - private void pushUp(int rt) { - max[rt] = Math.max(max[rt << 1], max[rt << 1 | 1]); - } - - // ln表示左子树元素结点个数,rn表示右子树结点个数 - private void pushDown(int rt, int ln, int rn) { - if (update[rt]) { - update[rt << 1] = true; - update[rt << 1 | 1] = true; - change[rt << 1] = change[rt]; - change[rt << 1 | 1] = change[rt]; - max[rt << 1] = change[rt]; - max[rt << 1 | 1] = change[rt]; - update[rt] = false; - } - } - - public void update(int L, int R, int C, int l, int r, int rt) { - if (L <= l && r <= R) { - update[rt] = true; - change[rt] = C; - max[rt] = C; - return; - } - int mid = (l + r) >> 1; - pushDown(rt, mid - l + 1, r - mid); - if (L <= mid) { - update(L, R, C, l, mid, rt << 1); - } - if (R > mid) { - update(L, R, C, mid + 1, r, rt << 1 | 1); - } - pushUp(rt); - } - - public int query(int L, int R, int l, int r, int rt) { - if (L <= l && r <= R) { - return max[rt]; - } - int mid = (l + r) >> 1; - pushDown(rt, mid - l + 1, r - mid); - int left = 0; - int right = 0; - if (L <= mid) { - left = query(L, R, l, mid, rt << 1); - } - if (R > mid) { - right = query(L, R, mid + 1, r, rt << 1 | 1); - } - return Math.max(left, right); - } - +```Go +package main + +import "math" + +// fallingSquares +func fallingSquares(positions [][]int) []int { + m := index(positions) + // 100 -> 1 306 -> 2 403 -> 3 + // [100,403] 1~3 + N := len(m) // 1 ~ N + var res []int + origin := make([]int, N) + max := 0 + sgTree := InitSegmentTree(origin) + // 每落一个正方形,收集一下,所有东西组成的图像,最高高度是什么 + for _, arr := range positions { + L := m[arr[0]] + R := m[arr[0] + arr[1] - 1] + height := sgTree.GetSum(L, R, 1, N, 1) + arr[1] + max = int(math.Max(float64(max), float64(height))) + res = append(res, max) + sgTree.Update(L, R, height, 1, N, 1) } + return res +} - // positions - // [2,7] -> 表示位置从2开始,边长为7的方块,落下的x轴范围为2到8,不包括9是因为下一个位置为9可以落得下来; 2 , 8 - // [3, 10] -> 3, 12 - // - // 用treeSet做离散化,避免多申请空间 - public HashMap index(int[][] positions) { - TreeSet pos = new TreeSet<>(); - for (int[] arr : positions) { - pos.add(arr[0]); - pos.add(arr[0] + arr[1] - 1); - } - HashMap map = new HashMap<>(); - int count = 0; - for (Integer index : pos) { - map.put(index, ++count); - } - return map; +// positions +// [2,7] -> 表示位置从2开始,边长为7的方块,落下的x轴范围为2到8,不包括9是因为下一个位置为9可以落得下来; 2 , 8 +// [3, 10] -> 3, 12 +// +// 用treeSet做离散化,避免多申请空间 +func index(positions [][]int) map[int]int { + pos := make(map[int]string, 0) + for _, arr := range positions { + pos[arr[0]] = "" + pos[arr[0] + arr[1] - 1] = "" } - public List fallingSquares(int[][] positions) { - HashMap map = index(positions); - // 100 -> 1 306 -> 2 403 -> 3 - // [100,403] 1~3 - int N = map.size(); // 1 ~ N - SegmentTree segmentTree = new SegmentTree(N); - int max = 0; - List res = new ArrayList<>(); - // 每落一个正方形,收集一下,所有东西组成的图像,最高高度是什么 - for (int[] arr : positions) { - int L = map.get(arr[0]); - int R = map.get(arr[0] + arr[1] - 1); - int height = segmentTree.query(L, R, 1, N, 1) + arr[1]; - max = Math.max(max, height); - res.add(max); - segmentTree.update(L, R, height, 1, N, 1); - } - return res; + m := make(map[int]int, 0) + count := 0 + for key := range pos { + count++ + m[key] = count } - + return m } ``` From 46c6ba4c9ffa6ea37d64d25639d7a5f48c3785e6 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Mon, 4 Apr 2022 16:39:49 +0800 Subject: [PATCH 06/18] print table --- ...70\345\205\263\351\227\256\351\242\230.md" | 563 ++++++++++-------- 1 file changed, 314 insertions(+), 249 deletions(-) diff --git "a/19-\343\200\212\350\277\233\351\230\266\343\200\213\346\211\223\350\241\250\345\222\214\347\237\251\351\230\265\345\244\204\347\220\206\347\233\270\345\205\263\351\227\256\351\242\230.md" "b/19-\343\200\212\350\277\233\351\230\266\343\200\213\346\211\223\350\241\250\345\222\214\347\237\251\351\230\265\345\244\204\347\220\206\347\233\270\345\205\263\351\227\256\351\242\230.md" index 6c173db..36456fd 100644 --- "a/19-\343\200\212\350\277\233\351\230\266\343\200\213\346\211\223\350\241\250\345\222\214\347\237\251\351\230\265\345\244\204\347\220\206\347\233\270\345\205\263\351\227\256\351\242\230.md" +++ "b/19-\343\200\212\350\277\233\351\230\266\343\200\213\346\211\223\350\241\250\345\222\214\347\237\251\351\230\265\345\244\204\347\220\206\347\233\270\345\205\263\351\227\256\351\242\230.md" @@ -2,7 +2,7 @@ # 1 打表技巧和矩阵处理技巧 -在一个数组arr[]中,每个数的大小不超过1000,例如[10,9,6,12],所有的数,求所有数质数因子的个数总和? +在一个数组arr中,每个数的大小不超过1000,例如[10,9,6,12],所有的数,求所有数质数因子的个数总和? `10=2*5` @@ -21,8 +21,7 @@ 2)一个大问题解决时底层频繁使用规模不大的小问题的解,如果小问题的返回值满足条件1),可以把小问题的解列成一张表,作为程序的一部分 -3)打表找规律(本节课重点),有关1)和2)内容欢迎关注后序课程 - +3)打表找规律(本节课重点) ### 1.1.1 打表找规律 @@ -49,53 +48,76 @@ > 暴力思路,例如N=100个苹果,我们全部用8号袋装,最多使用12个8号袋子,剩4个苹果,6号袋没装满。8号袋减1,需要2个6号袋,满足。如果依次递减8号袋,为0个仍未有答案,则无解 -```Java -public class Code01_AppleMinBags { +```Go +package main - public static int minBags(int apple) { - if (apple < 0) { - return -1; - } - int bag6 = -1; - int bag8 = apple / 8; - int rest = apple - 8 * bag8; - while (bag8 >= 0 && rest < 24) { - int restUse6 = minBagBase6(rest); - if (restUse6 != -1) { - bag6 = restUse6; - break; - } - rest = apple - 8 * (--bag8); +import "fmt" + +func minBags(apple int) int { + if apple < 0 { + return -1 + } + + bag6 := -1 + bag8 := apple / 8 + rest := apple - 8 * bag8 + for bag8 >= 0 && rest < 24 { + restUse6 := minBagBase6(rest) + if restUse6 != -1 { + bag6 = restUse6 + break } - return bag6 == -1 ? -1 : bag6 + bag8; + + rest = apple - 8 * (bag8) + bag8-- + } + if bag6 == -1 { + return -1 + } else { + return bag6 + bag8 + } +} + +// 如果剩余苹果rest可以被装6个苹果的袋子搞定,返回袋子数量 +// 不能搞定返回-1 +func minBagBase6(rest int) int { + if rest % 6 == 0 { + return rest / 6 + } else { + return -1 } +} - // 如果剩余苹果rest可以被装6个苹果的袋子搞定,返回袋子数量 - // 不能搞定返回-1 - public static int minBagBase6(int rest) { - return rest % 6 == 0 ? (rest / 6) : -1; +// 根据打表规律写code +func minBagAwesome(apple int) int { + if apple & 1 != 0 {// 如果是奇数,返回-1 + return -1 } - // 根据打表规律写code - public static int minBagAwesome(int apple) { - if ((apple & 1) != 0) { // 如果是奇数,返回-1 - return -1; - } - if (apple < 18) { - return apple == 0 ? 0 : (apple == 6 || apple == 8) ? 1 - : (apple == 12 || apple == 14 || apple == 16) ? 2 : -1; + if apple < 18 { + if apple == 0 { + return 0 + } else { + if apple == 6 || apple == 8 { + return 1 + } else { + if apple == 12 || apple == 24 || apple == 16 { + return 2 + } else { + return -1 + } + } } - return (apple - 18) / 8 + 3; } - // 打表看规律,摒弃数学规律 - public static void main(String[] args) { - for(int apple = 1; apple < 100;apple++) { - System.out.println(apple + " : "+ minBags(apple)); - } + return (apple - 18) / 8 + 3 +} +// 打表看规律,摒弃数学规律 +func main() { + for apple:=1; apple < 100; apple++ { + fmt.Println(minBags(apple)) } - } ``` @@ -116,52 +138,57 @@ public class Code01_AppleMinBags { > 暴力思路打表找规律 -```Java -public class Code02_EatGrass { - - // n份青草放在一堆 - // 先手后手都绝顶聪明 - // string "先手" "后手" - public static String winner1(int n) { - // 0 1 2 3 4 - // 后 先 后 先 先 - // base case - if (n < 5) { // base case - return (n == 0 || n == 2) ? "后手" : "先手"; - } - // n >= 5 时 - int base = 1; // 当前先手决定吃的草数 - // 当前是先手在选 - while (base <= n) { - // 当前一共n份草,先手吃掉的是base份,n - base 是留给后手的草 - // 母过程 先手 在子过程里是 后手 - if (winner1(n - base).equals("后手")) { - return "先手"; - } - if (base > n / 4) { // 防止base*4之后溢出 - break; - } - base *= 4; +``` +package main + +import "fmt" + +// n份青草放在一堆 +// 先手后手都绝顶聪明 +// string "先手" "后手" +func winner1(n int) string { + // 0 1 2 3 4 + // 后 先 后 先 先 + // base case + if n < 5 { + if n == 0 || n == 2 { + return "后手" + } else { + return "先手" } - return "后手"; } - // 根据打表的规律,写代码 - public static String winner2(int n) { - if (n % 5 == 0 || n % 5 == 2) { - return "后手"; - } else { - return "先手"; + // n >= 5 时 + base := 1 // 当前先手决定吃的草数 + // 当前是先手在选 + for base <= n { + // 当前一共n份草,先手吃掉的是base份,n - base 是留给后手的草 + // 母过程 先手 在子过程里是 后手 + if winner1(n -base) == "后手" { + return "先手" } + if base > n / 4 { // 防止base*4之后溢出 + break + } + base *= 4 } + return "后手" +} - // 暴力打表找规律 - public static void main(String[] args) { - for (int i = 0; i <= 50; i++) { - System.out.println(i + " : " + winner1(i)); - } +// 根据打表的规律,写代码 +func winner2(n int) string { + if n % 5 == 0 || n % 5 == 2 { + return "后手" + } else { + return "先手" } +} +// 暴力打表找规律 +func main() { + for i:=0; i<=50; i++ { + fmt.Println(fmt.Sprintf("%d : %s", i, winner1(i))) + } } ``` @@ -181,48 +208,48 @@ public class Code02_EatGrass { 给定一个参数N,返回是不是可以表示成若干连续正数和的数 -```Java -public class Code03_MSumToN { +```Go +package main - // 暴力法 - public static boolean isMSum1(int num) { - for (int i = 1; i <= num; i++) { - int sum = i; - for (int j = i + 1; j <= num; j++) { - if (sum + j > num) { - break; - } - if (sum + j == num) { - return true; - } - sum += j; +import "fmt" + +// isMSum1 暴力法。给定一个参数N,返回是不是可以表示成若干连续正数和的数 +func isMSum1(num int) bool { + for i := 1; i<=num; i++ { + sum := i + for j := i + 1; j <= num; j++ { + if sum + j > num { + break } + if sum + j == num { + return true + } + sum += j } - return false; } + return false +} - // 根据打表的规律写代码 - public static boolean isMSum2(int num) { - if (num < 3) { - return false; - } - return (num & (num - 1)) != 0; +// 根据打表的规律写代码 +func isMSum2(num int) bool { + if num < 3 { + return false } + return (num & (num - 1)) != 0 +} - // 打表 - public static void main(String[] args) { - for (int num = 1; num < 200; num++) { - System.out.println(num + " : " + isMSum1(num)); - } - System.out.println("test begin"); - for (int num = 1; num < 5000; num++) { - if (isMSum1(num) != isMSum2(num)) { - System.out.println("Oops!"); - } +// 打表 +func main() { + for num := 1; num <200; num++ { + fmt.Println(fmt.Sprintf("%d : %v", num, isMSum1(num))) + } + fmt.Println("test begin") + for num := 1; num < 5000; num++ { + if isMSum1(num) != isMSum2(num) { + fmt.Println("Oops!") } - System.out.println("test end"); - } + fmt.Println("test end") } ``` @@ -255,59 +282,71 @@ public class Code03_MSumToN { > 思路:准备A和B两个点,坐标都指向0,0位置。A和B同时走,A往右走,走到尽头后往下走,B往下走,走到不能再走了往右走。通过这么处理,A和B每个位置的连线都是一条斜线,且无重复。A和B每同时走一步,打印每次逆序打印,即开始时从B往A打印,下一步从A往B打印,循环往复 -```Java -public class Code06_ZigZagPrintMatrix { - - public static void printMatrixZigZag(int[][] matrix) { - // A的行row - int tR = 0; - // A的列coulum - int tC = 0; - // B的行row - int dR = 0; - // B的列coulum - int dC = 0; - // 终止位置的行和列 - int endR = matrix.length - 1; - int endC = matrix[0].length - 1; - // 是不是从右上往左下打印 - boolean fromUp = false; - // A的轨迹不会超过最后一行 - while (tR != endR + 1) { - // 告诉当前A和B,打印方向,完成打印 - printLevel(matrix, tR, tC, dR, dC, fromUp); - // 打印完之后,A和B再移动。A到最右再向下,B到最下再向右 - tR = tC == endC ? tR + 1 : tR; - tC = tC == endC ? tC : tC + 1; - dC = dR == endR ? dC + 1 : dC; - dR = dR == endR ? dR : dR + 1; - // A和B来到下一个位置之后,改变打印方向 - fromUp = !fromUp; +```Go +package main + +import "fmt" + +func printMatrixZigZag(matrix [][]int) { + // A的行row + tR := 0 + // A的列coulum + tC := 0 + // B的行row + dR := 0 + // B的列coulum + dC := 0 + // 终止位置的行和列 + endR := len(matrix) - 1 + endC := len(matrix[0]) - 1 + // 是不是从右上往左下打印 + fromUp := false + // A的轨迹不会超过最后一行 + for tR != endR + 1 { + // 告诉当前A和B,打印方向,完成打印 + printLevel(matrix, tR, tC, dR, dC, fromUp) + // 打印完之后,A和B再移动。A到最右再向下,B到最下再向右 + if tC == endC { + tR = tR + 1 + } else { + tC = tC + 1 } - System.out.println(); - } - - public static void printLevel(int[][] m, int tR, int tC, int dR, int dC, - boolean f) { - if (f) { - while (tR != dR + 1) { - System.out.print(m[tR++][tC--] + " "); - } + if dR == endR { + dC = dC + 1 } else { - while (dR != tR - 1) { - System.out.print(m[dR--][dC++] + " "); - } + dR = dR + 1 } + // A和B来到下一个位置之后,改变打印方向 + fromUp = !fromUp } + fmt.Println() +} - public static void main(String[] args) { - int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }; - printMatrixZigZag(matrix); - +func printLevel(m [][]int, tR int, tC int, dR int, dC int, f bool) { + if f { + for tR != dR + 1 { + fmt.Print(fmt.Sprintf("%d ", m[tR][tC])) + tR++ + tC-- + } + } else { + for dR != tR -1 { + fmt.Print(fmt.Sprintf("%d ", m[dR][dC])) + dR-- + dC++ + } } - } +// 1 2 5 9 6 3 4 7 10 11 8 12 +func main() { + matrix := [][]int{ + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 }, + } + printMatrixZigZag(matrix) +} ``` #### 转圈打印矩阵 @@ -326,64 +365,72 @@ public class Code06_ZigZagPrintMatrix { > 思路:每个圈,我们知道左上角的位置,和右下角的位置,我们就可以得到需要转圈的圈的大小, -```Java -public class Code05_PrintMatrixSpiralOrder { - - public static void spiralOrderPrint(int[][] matrix) { - // A行 - int tR = 0; - // A列 - int tC = 0; - // B行 - int dR = matrix.length - 1; - // B列 - int dC = matrix[0].length - 1; - - while (tR <= dR && tC <= dC) { - printEdge(matrix, tR++, tC++, dR--, dC--); - } +```Go +package main + +import "fmt" + +// 转圈打印矩阵 +func spiralOrderPrint(matrix [][]int) { + // A行 + tR := 0 + // A列 + tC := 0 + // B行 + dR := len(matrix) - 1 + // B列 + dC := len(matrix[0]) - 1 + for tR <= dR && tC <= dC { + printEdge(matrix, tR, tC, dR, dC) + tR++ + tC++ + dR-- + dC-- } +} - // 当前打印,左上角和右下角的位置 - public static void printEdge(int[][] m, int tR, int tC, int dR, int dC) { - // 表示区域只剩下一条横线的时候 - if (tR == dR) { - for (int i = tC; i <= dC; i++) { - System.out.print(m[tR][i] + " "); - } - } else if (tC == dC) { // 表示区域只剩下一条竖线了 - for (int i = tR; i <= dR; i++) { - System.out.print(m[i][tC] + " "); - } - } else { - int curC = tC; - int curR = tR; - while (curC != dC) { - System.out.print(m[tR][curC] + " "); - curC++; - } - while (curR != dR) { - System.out.print(m[curR][dC] + " "); - curR++; - } - while (curC != tC) { - System.out.print(m[dR][curC] + " "); - curC--; - } - while (curR != tR) { - System.out.print(m[curR][tC] + " "); - curR--; - } +// 当前打印,左上角和右下角的位置 +func printEdge(m [][]int, tR int, tC int, dR int, dC int) { + // 表示区域只剩下一条横线的时候 + if tR == dR { + for i:=tC; i<=dC; i++ { + fmt.Print(fmt.Sprintf("%d ", m[tR][i])) + } + } else if tC == dC { // 表示区域只剩下一条竖线了 + for i:=tR; i<=dR; i++ { + fmt.Print(fmt.Sprintf("%d ", m[i][tC])) + } + } else { // 通用情况 + curC := tC + curR := tR + for curC != dC { + fmt.Print(fmt.Sprintf("%d ", m[tR][curC])) + curC++ + } + for curR != dR { + fmt.Print(fmt.Sprintf("%d ", m[curR][dC])) + curR++ + } + for curC != tC { + fmt.Print(fmt.Sprintf("%d ", m[dR][curC])) + curC-- + } + for curR != tR { + fmt.Print(fmt.Sprintf("%d ", m[curR][tC])) + curR-- } } +} - public static void main(String[] args) { - int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, - { 13, 14, 15, 16 } }; - spiralOrderPrint(matrix); - +// 1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10 +func main() { + matrix := [][]int{ + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 }, } - + spiralOrderPrint(matrix) } ``` @@ -415,56 +462,74 @@ public class Code05_PrintMatrixSpiralOrder { > 思路:一圈一圈的转,和旋转打印思路比较像。按圈,再按小组旋转,第一圈的第一个小组为四个角。分别为:1,4,16,13;第二小组为:2,8,15,9;依次旋转小组,最终达到旋转该圈的目的。接着旋转下一个圈的各个小组。每一层的小组数目等于该圈的边长减1 -```Java -public class Code04_RotateMatrix { - - public static void rotate(int[][] matrix) { - // a行 - int a = 0; - // b列 - int b = 0; - // c行 - int c = matrix.length - 1; - // d列 - int d = matrix[0].length - 1; - // 由于是正方形矩阵,只需要判断行不越界,等同于判断列不越界 - while (a < c) { - rotateEdge(matrix, a++, b++, c--, d--); - } +```Go +package main + +import "fmt" + +// 原地旋转正方形矩阵 +func rotate(matrix [][]int) { + // a行 + a := 0 + // b列 + b := 0 + // c行 + c := len(matrix) - 1 + // d列 + d := len(matrix[0]) - 1 + // 由于是正方形矩阵,只需要判断行不越界,等同于判断列不越界 + for a < c { + rotateEdge(matrix, a, b, c, d) + a++ + b++ + c-- + d-- } +} - // 当前需要转的圈的左上角和右下角 - public static void rotateEdge(int[][] m, int a, int b, int c, int d) { - int tmp = 0; - // 得到左上角右下角坐标,我们可以知道右上角和左下角的位置,这四个位置先旋转。这四个位置称为一个小组。 - // 旋转完之后,找下四个位置的小组再旋转 - for (int i = 0; i < d - b; i++) { - tmp = m[a][b + i]; - m[a][b + i] = m[c - i][b]; - m[c - i][b] = m[c][d - i]; - m[c][d - i] = m[a + i][d]; - m[a + i][d] = tmp; - } +// 当前需要转的圈的左上角和右下角 +func rotateEdge(m [][]int, a, b, c, d int) { + tmp := 0 + // 得到左上角右下角坐标,我们可以知道右上角和左下角的位置,这四个位置先旋转。这四个位置称为一个小组。 + // 旋转完之后,找下四个位置的小组再旋转 + for i := 0; i < d - b; i++ { + tmp = m[a][b + i] + m[a][b + i] = m[c - i][b] + m[c - i][b] = m[c][d - i] + m[c][d - i] = m[a + i][d] + m[a + i][d] = tmp } +} - public static void printMatrix(int[][] matrix) { - for (int i = 0; i != matrix.length; i++) { - for (int j = 0; j != matrix[0].length; j++) { - System.out.print(matrix[i][j] + " "); - } - System.out.println(); +func printMatrix(matrix [][]int) { + for i := 0; i != len(matrix); i++ { + for j := 0; j != len(matrix[0]); j++ { + fmt.Print(fmt.Sprintf("%d ", matrix[i][j])) } + fmt.Println() } +} - public static void main(String[] args) { - int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, { 13, 14, 15, 16 } }; - printMatrix(matrix); - rotate(matrix); - System.out.println("========="); - printMatrix(matrix); - +//1 2 3 4 +//5 6 7 8 +//9 10 11 12 +//13 14 15 16 +//============== +//13 9 5 1 +//14 10 6 2 +//15 11 7 3 +//16 12 8 4 +func main() { + matrix := [][]int{ + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 }, } - + printMatrix(matrix) + rotate(matrix) + fmt.Println("==============") + printMatrix(matrix) } ``` From 962ed8ba3ce860c3b2e6335c3b1ff0275e4e1f30 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Mon, 4 Apr 2022 16:40:08 +0800 Subject: [PATCH 07/18] sum arr --- ...40\345\222\214\351\227\256\351\242\230.md" | 418 +++++++----------- 1 file changed, 153 insertions(+), 265 deletions(-) diff --git "a/20-\343\200\212\350\277\233\351\230\266\343\200\213\346\225\260\347\273\204\347\264\257\345\212\240\345\222\214\351\227\256\351\242\230.md" "b/20-\343\200\212\350\277\233\351\230\266\343\200\213\346\225\260\347\273\204\347\264\257\345\212\240\345\222\214\351\227\256\351\242\230.md" index c64fa94..e201374 100644 --- "a/20-\343\200\212\350\277\233\351\230\266\343\200\213\346\225\260\347\273\204\347\264\257\345\212\240\345\222\214\351\227\256\351\242\230.md" +++ "b/20-\343\200\212\350\277\233\351\230\266\343\200\213\346\225\260\347\273\204\347\264\257\345\212\240\345\222\214\351\227\256\351\242\230.md" @@ -10,7 +10,7 @@ > 解决思路:定义一台服务器,对世界的国家进行分类,比如中国下是省份,美国下是州,英国下是邦。每一个国家向中央服务器要随机范围,中央服务器分配出去的是start和range。比如给中国分配的是start从1开始,range到100w,中国uuid不够用了,可以再向中央服务器要,分配后中央服务器的start要增大到已分配出去后的位置。其他国家类似 -> 该设计师垂直扩展的技术,当前很多有事水平扩展,比如直接hashcode,random等。但有些场景适合用这种垂直扩展的解决方案 +> 该设计是垂直扩展的技术,当前很多是水平扩展,比如直接hashcode,random等。但有些场景适合用这种垂直扩展的解决方案 @@ -18,7 +18,7 @@ ### 1.1.1 第一连例题 -有一个全是正数的数组,和一个正数sum。求该数组的累加和等于sum的子数组多长。例如[3,2,1,1,1,6,1,1,1,1,1,1],sum等于6。最长的子数组为[1,1,1,1,1,1]返回长度6 +有一个全是正数的数组,和一个正数sum。求该数组的累加和等于sum的子数组最长多长。例如[3,2,1,1,1,6,1,1,1,1,1,1],sum等于6。最长的子数组为[1,1,1,1,1,1]返回长度6 > 由于是正数数组,累加和和范围具有单调性。对于具有单调性的题目,要么定义左右指针,要么定义窗口滑动 @@ -32,208 +32,96 @@ 3、如果windowSum等于sum,此时的窗口大小就是一个满足条件的子数组大小,决定是否要更新答案; -```Java -public class Code01_LongestSumSubArrayLengthInPositiveArray { +```Go +package main - // 滑动窗口的表示 - public static int getMaxLength(int[] arr, int K) { - if (arr == null || arr.length == 0 || K <= 0) { - return 0; - } - // 初始窗口位置[0,0],窗口当前只有第一个数 - int left = 0; - int right = 0; - int sum = arr[0]; - int len = 0; - while (right < arr.length) { - // 窗口的累加和sum等于我们的目标k。求窗口大小len - if (sum == K) { - len = Math.max(len, right - left + 1); - // 窗口累加和减去左窗口位置的值,左位置再出窗口 - sum -= arr[left++]; - } else if (sum < K) { - // 窗口右边界扩,如果不越界把扩之后的那个位置的值加到窗口累加值上 - right++; - if (right == arr.length) { - break; - } - sum += arr[right]; - } else { - sum -= arr[left++]; - } - } - return len; - } - - // for test - public static int right(int[] arr, int K) { - int max = 0; - for (int i = 0; i < arr.length; i++) { - for (int j = i; j < arr.length; j++) { - if (valid(arr, i, j, K)) { - max = Math.max(max, j - i + 1); - } - } - } - return max; - } +import ( + "fmt" + "math" +) - // for test - public static boolean valid(int[] arr, int L, int R, int K) { - int sum = 0; - for (int i = L; i <= R; i++) { - sum += arr[i]; - } - return sum == K; +// getMaxLength 有一个全是正数的数组,和一个正数sum。求该数组的累加和等于sum的子数组最长多长。 滑动窗口的表示 +func getMaxLength(arr []int, K int) int { + if len(arr) == 0 || K <= 0 { + return 0 } - // for test - public static int[] generatePositiveArray(int size, int value) { - int[] ans = new int[size]; - for (int i = 0; i != size; i++) { - ans[i] = (int) (Math.random() * value) + 1; - } - return ans; - } - - // for test - public static void printArray(int[] arr) { - for (int i = 0; i != arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - public static void main(String[] args) { - int len = 50; - int value = 100; - int testTime = 500000; - System.out.println("test begin"); - for (int i = 0; i < testTime; i++) { - int[] arr = generatePositiveArray(len, value); - int K = (int) (Math.random() * value) + 1; - int ans1 = getMaxLength(arr, K); - int ans2 = right(arr, K); - if (ans1 != ans2) { - System.out.println("Oops!"); - printArray(arr); - System.out.println("K : " + K); - System.out.println(ans1); - System.out.println(ans2); - break; + // 初始窗口位置[0,0],窗口当前只有第一个数 + left := 0 + right := 0 + sum := arr[0] + length := 0 + + for right < len(arr) { + // 窗口的累加和sum等于我们的目标k。求窗口大小len + if sum == K { + length = int(math.Max(float64(length), float64(right - left + 1))) + // 窗口累加和减去左窗口位置的值,左位置再出窗口 + sum -= arr[left] + left++ + } else if sum < K { + // 窗口右边界扩,如果不越界把扩之后的那个位置的值加到窗口累加值上 + right++ + if right == len(arr) { + break } + sum += arr[right] + } else { + sum -= arr[left] + left++ } - System.out.println("test end"); } - + return length } +// 最长的子数组为[1,1,1,1,1,1]返回长度6 +func main() { + arr := []int{3,2,1,1,1,6,1,1,1,1,1,1} + sum := 6 + fmt.Println(getMaxLength(arr, sum)) +} ``` ### 1.1.2 第二连例题 - 有一个数组,值可以为正可以为负可以为0。给定一个值sum,求子数组中累加和等于sum的最大长度? - > 该题和第一连问题的区别是,数组的值可正可负可零,单调性消失了。对于数组问题,我们常见的解决子数组的思考思路,如果以每一个位置开头能求出一个答案,那么目标答案一定在其中。反过来如果以每一个位置为结尾能求出一个答案,那么目标答案一定也在其中 - > 该题思路用第二种比较方便,我们以某个位置i结尾,之前的数累加和等于目标sum,求该位置满足此条件的最长数组。该种思路等同于,从0位置开始到i位置的累加和(allSum),减去从0位置到最早和0位置的累加和等于allSum-sum的位置j。那么原问题的答案是j+1到j位置的长度。预置,0位置累加和位置等于-1位置 -```Java -public class Code02_LongestSumSubArrayLength { +```Go +package main - // arr数组,累加和为k的最长子数组返回 - public static int maxLength(int[] arr, int k) { - if (arr == null || arr.length == 0) { - return 0; - } - // key表示累加和,value表示最早出现的位置 - HashMap map = new HashMap(); - // 0位置的累加和,最早出现在-1位置。预置 - map.put(0, -1); // important - // 最大长度是多少 - int len = 0; - // 累加和多大 - int sum = 0; - for (int i = 0; i < arr.length; i++) { - sum += arr[i]; - if (map.containsKey(sum - k)) { - // j+1到i有多少个数,i-j个 - len = Math.max(i - map.get(sum - k), len); - } - if (!map.containsKey(sum)) { - map.put(sum, i); - } - } - return len; - } +import "math" - // for test - public static int right(int[] arr, int K) { - int max = 0; - for (int i = 0; i < arr.length; i++) { - for (int j = i; j < arr.length; j++) { - if (valid(arr, i, j, K)) { - max = Math.max(max, j - i + 1); - } - } - } - return max; +// 有一个数组,值可以为正可以为负可以为0。给定一个值sum,求子数组中累加和等于sum的最大长度 +// arr数组,累加和为k的最长子数组返回 +func maxLength(arr []int, k int) int { + if len(arr) == 0 { + return 0 } - // for test - public static boolean valid(int[] arr, int L, int R, int K) { - int sum = 0; - for (int i = L; i <= R; i++) { - sum += arr[i]; + // key表示累加和,value表示最早出现的位置 + m := make(map[int]int, 0) + // 0位置的累加和,最早出现在-1位置。预置 + m[0] = -1 // important + // 最大长度是多少 + length := 0 + // 累加和多大 + sum := 0 + for i := 0; i < len(arr); i++ { + sum += arr[i] + if v1, ok := m[sum - k]; ok { + // j+1到i有多少个数,i-j个 + length = int(math.Max(float64(i - v1), float64(length))) } - return sum == K; - } - - // for test - public static int[] generateRandomArray(int size, int value) { - int[] ans = new int[(int) (Math.random() * size) + 1]; - for (int i = 0; i < ans.length; i++) { - ans[i] = (int) (Math.random() * value) - (int) (Math.random() * value); + if _, ok := m[sum]; !ok { + m[sum] = i } - return ans; } - - // for test - public static void printArray(int[] arr) { - for (int i = 0; i != arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - public static void main(String[] args) { - int len = 50; - int value = 100; - int testTime = 500000; - - System.out.println("test begin"); - for (int i = 0; i < testTime; i++) { - int[] arr = generateRandomArray(len, value); - int K = (int) (Math.random() * value) - (int) (Math.random() * value); - int ans1 = maxLength(arr, K); - int ans2 = right(arr, K); - if (ans1 != ans2) { - System.out.println("Oops!"); - printArray(arr); - System.out.println("K : " + K); - System.out.println(ans1); - System.out.println(ans2); - break; - } - } - System.out.println("test end"); - - } - + return length } ``` @@ -265,109 +153,109 @@ public class Code02_LongestSumSubArrayLength { > 该题巧妙之处是排除可能性,比较难 -```Java -public class Code03_LongestLessSumSubArrayLength { +```Go +package main - public static int maxLengthAwesome(int[] arr, int k) { - if (arr == null || arr.length == 0) { - return 0; - } - int[] minSums = new int[arr.length]; - int[] minSumEnds = new int[arr.length]; - minSums[arr.length - 1] = arr[arr.length - 1]; - minSumEnds[arr.length - 1] = arr.length - 1; - for (int i = arr.length - 2; i >= 0; i--) { - if (minSums[i + 1] < 0) { - minSums[i] = arr[i] + minSums[i + 1]; - minSumEnds[i] = minSumEnds[i + 1]; - } else { - minSums[i] = arr[i]; - minSumEnds[i] = i; - } - } - int end = 0; - int sum = 0; - int res = 0; - // i是窗口的最左的位置,end扩出来的最右有效块儿的最后一个位置的,再下一个位置 - // end也是下一块儿的开始位置 - // 窗口:[i~end) - for (int i = 0; i < arr.length; i++) { - // while循环结束之后: - // 1) 如果以i开头的情况下,累加和<=k的最长子数组是arr[i..end-1],看看这个子数组长度能不能更新res; - // 2) 如果以i开头的情况下,累加和<=k的最长子数组比arr[i..end-1]短,更新还是不更新res都不会影响最终结果; - while (end < arr.length && sum + minSums[end] <= k) { - sum += minSums[end]; - end = minSumEnds[end] + 1; - } - res = Math.max(res, end - i); - if (end > i) { // 窗口内还有数 [i~end) [4,4) - sum -= arr[i]; - } else { // 窗口内已经没有数了,说明从i开头的所有子数组累加和都不可能<=k - end = i + 1; - } - } - return res; +import ( + "fmt" + "math" +) + +// 一个数组arr中,有正数,有负数,有0。给定累加和目标k,求所有子数组累加和中小于等于k的数组的最大长度 +func maxLengthAwesome(arr []int, k int) int { + if len(arr) == 0 { + return 0 } - public static int maxLength(int[] arr, int k) { - int[] h = new int[arr.length + 1]; - int sum = 0; - h[0] = sum; - for (int i = 0; i != arr.length; i++) { - sum += arr[i]; - h[i + 1] = Math.max(sum, h[i]); - } - sum = 0; - int res = 0; - int pre = 0; - int len = 0; - for (int i = 0; i != arr.length; i++) { - sum += arr[i]; - pre = getLessIndex(h, sum - k); - len = pre == -1 ? 0 : i - pre + 1; - res = Math.max(res, len); + minSums := make([]int, len(arr)) + minSumEnds := make([]int, len(arr)) + minSums[len(arr) - 1] = arr[len(arr) - 1] + minSumEnds[len(arr) - 1] = len(arr) - 1 + + for i := len(arr) - 2; i >= 0; i-- { + if minSums[i + 1] < 0 { + minSums[i] = arr[i] + minSums[i + 1] + minSumEnds[i] = minSumEnds[i + 1] + } else { + minSums[i] = arr[i] + minSumEnds[i] = i } - return res; } - public static int getLessIndex(int[] arr, int num) { - int low = 0; - int high = arr.length - 1; - int mid = 0; - int res = -1; - while (low <= high) { - mid = (low + high) / 2; - if (arr[mid] >= num) { - res = mid; - high = mid - 1; - } else { - low = mid + 1; - } + end := 0 + sum := 0 + res := 0 + // i是窗口的最左的位置,end扩出来的最右有效块儿的最后一个位置的,再下一个位置 + // end也是下一块儿的开始位置 + // 窗口:[i~end) + for i := 0; i < len(arr); i++ { + // for循环结束之后: + // 1) 如果以i开头的情况下,累加和<=k的最长子数组是arr[i..end-1],看看这个子数组长度能不能更新res; + // 2) 如果以i开头的情况下,累加和<=k的最长子数组比arr[i..end-1]短,更新还是不更新res都不会影响最终结果; + for end < len(arr) && sum + minSums[end] <= k { + sum += minSums[end] + end = minSumEnds[end] + 1 + } + res = int(math.Max(float64(res), float64(end - i))) + if end > i { // 窗口内还有数 [i~end) [4,4) + sum -= arr[i] + } else { // 窗口内已经没有数了,说明从i开头的所有子数组累加和都不可能<=k + end = i + 1 } - return res; } + return res +} - // for test - public static int[] generateRandomArray(int len, int maxValue) { - int[] res = new int[len]; - for (int i = 0; i != res.length; i++) { - res[i] = (int) (Math.random() * maxValue) - (maxValue / 3); +// 一个数组arr中,有正数,有负数,有0。给定累加和目标k,求所有子数组累加和中小于等于k的数组的最大长度; 暴力解法 +func maxLength2(arr []int, k int) int { + h := make([]int, len(arr) + 1) + sum := 0 + h[0] = sum + for i := 0; i != len(arr); i++ { + sum += arr[i] + h[i + 1] = int(math.Max(float64(sum), float64(h[i]))) + } + sum = 0 + res := 0 + pre := 0 + length := 0 + for i := 0; i != len(arr); i++ { + sum += arr[i] + pre = getLessIndex(h, sum - k) + if pre == -1 { + length = 0 + } else { + length = i - pre + 1 } - return res; + res = int(math.Max(float64(res), float64(length))) } + return res +} - public static void main(String[] args) { - System.out.println("test begin"); - for (int i = 0; i < 10000000; i++) { - int[] arr = generateRandomArray(10, 20); - int k = (int) (Math.random() * 20) - 5; - if (maxLengthAwesome(arr, k) != maxLength(arr, k)) { - System.out.println("Oops!"); - } +func getLessIndex(arr []int, num int) int { + low := 0 + high := len(arr) - 1 + mid := 0 + res := -1 + for low <= high { + mid = (low + high) / 2 + if arr[mid] >= num { + res = mid + high = mid - 1 + } else { + low = mid + 1 } - System.out.println("test finish"); } + return res +} +//7 +//7 +func main() { + arr := []int{3,7,4,-6,6,3,-2,0,7-3,2} + k := 10 + fmt.Println(maxLength2(arr, k)) + fmt.Println(maxLengthAwesome(arr, k)) } ``` From 57385c1b5a3eb3bde2cc830fe825fc0cfb5856d1 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Mon, 4 Apr 2022 16:44:58 +0800 Subject: [PATCH 08/18] order table --- ...12\345\205\266\345\216\237\347\220\206.md" | 2644 ++++++----------- 1 file changed, 870 insertions(+), 1774 deletions(-) diff --git "a/23-\343\200\212\350\277\233\351\230\266\343\200\213\346\234\211\345\272\217\350\241\250\344\273\213\347\273\215\345\217\212\345\205\266\345\216\237\347\220\206.md" "b/23-\343\200\212\350\277\233\351\230\266\343\200\213\346\234\211\345\272\217\350\241\250\344\273\213\347\273\215\345\217\212\345\205\266\345\216\237\347\220\206.md" index 4cf7bd3..af43197 100644 --- "a/23-\343\200\212\350\277\233\351\230\266\343\200\213\346\234\211\345\272\217\350\241\250\344\273\213\347\273\215\345\217\212\345\205\266\345\216\237\347\220\206.md" +++ "b/23-\343\200\212\350\277\233\351\230\266\343\200\213\346\234\211\345\272\217\350\241\250\344\273\213\347\273\215\345\217\212\345\205\266\345\216\237\347\220\206.md" @@ -6,29 +6,22 @@ 经典的搜索二叉树,是没有重复值的,任何节点为头的数,左孩子都比自己小,右孩子都比自己大 - 允许重复值的改进的搜索二叉树,可以在每个节点上增加一个统计词频的数据项。表示出现了几次;但是不可相等的放到左右孩子上,搜索二叉树变平衡时,会影响后续的旋转 - 1、搜索二叉树一定要说明以什么标准来排序 2、经典的搜索二叉树,树上没有重复的用来排序的key值 3、如果有重复节点的需求,可以在一个节点内部增加数据项 - - ## 1.2 搜索二叉树的增删改查 - ### 1.2.1 搜索二叉树的查找和添加 - 查找 搜索二叉树查询key(查询某个key存在还是不存在),当前节点比自己小,到右子树上去找,当前节点比自己大,到其左孩子上去找,越界,说明不存在 - - 1、如果当前节点的value==key,返回true 2、如果当前节点的value 一个节点的后继节点,就是该节点右孩子的最左的那个节点。 ``` graph TD @@ -82,413 +75,228 @@ graph TD 8-->7 ``` -```Java -package class05; - -/** - * Not implemented by zuochengyun - * - * Abstract binary search tree implementation. Its basically fully implemented - * binary search tree, just template method is provided for creating Node (other - * trees can have slightly different nodes with more info). This way some code - * from standart binary search tree can be reused for other kinds of binary - * trees. - * - * @author Ignas Lelys - * @created Jun 29, 2011 - * - */ -public class AbstractBinarySearchTree { - - /** Root node where whole tree starts. */ - public Node root; - - /** Tree size. */ - protected int size; - - /** - * Because this is abstract class and various trees have different - * additional information on different nodes subclasses uses this abstract - * method to create nodes (maybe of class {@link Node} or maybe some - * different node sub class). - * - * @param value - * Value that node will have. - * @param parent - * Node's parent. - * @param left - * Node's left child. - * @param right - * Node's right child. - * @return Created node instance. - */ - protected Node createNode(int value, Node parent, Node left, Node right) { - return new Node(value, parent, left, right); - } - - /** - * Finds a node with concrete value. If it is not found then null is - * returned. - * 查找节点 - * - * @param element - * Element value. - * @return Node with value provided, or null if not found. - */ - public Node search(int element) { - Node node = root; - while (node != null && node.value != null && node.value != element) { - // 小于当前节点,找左孩子对比 - if (element < node.value) { - node = node.left; - } else { - // 大于当前节点,找右孩子对比 - node = node.right; - } - } - return node; - } - - /** - * Insert new element to tree. - * 插入一个节点 - * - * @param element - * Element to insert. - */ - public Node insert(int element) { - // 首先如果这个树是空的,把该节点当成头节点 - if (root == null) { - root = createNode(element, null, null, null); - size++; - return root; - } +```Go +package main - // 需要插入在该节点下面 - Node insertParentNode = null; - Node searchTempNode = root; - while (searchTempNode != null && searchTempNode.value != null) { - insertParentNode = searchTempNode; - if (element < searchTempNode.value) { - searchTempNode = searchTempNode.left; - } else { - searchTempNode = searchTempNode.right; - } - } +import "fmt" - Node newNode = createNode(element, insertParentNode, null, null); - if (insertParentNode.value > newNode.value) { - insertParentNode.left = newNode; - } else { - insertParentNode.right = newNode; - } +type Node struct { + Value int + Left *Node // 左孩子指针 + Right *Node // 右孩子指针 + Parent *Node // 指向父亲的指针 +} - size++; - return newNode; - } - - /** - * Removes element if node with such value exists. - * 删除节点,每个节点由于加入向上的指针,那么旋转的时候会方便些 - * - * @param element - * Element value to remove. - * - * @return New node that is in place of deleted node. Or null if element for - * delete was not found. - */ - public Node delete(int element) { - Node deleteNode = search(element); - if (deleteNode != null) { - return delete(deleteNode); - } else { - return null; - } +// BinarySearchTree 二叉搜索树 +type BinarySearchTree struct { + Root *Node + Size int +} + +// createNode 构建一个树节点 +func createNode(value int, parent *Node, left *Node, right *Node) *Node { + return &Node{ + Value: value, + Left: left, + Right: right, + Parent: parent, } +} - /** - * Delete logic when node is already found. - * - * @param deleteNode - * Node that needs to be deleted. - * - * @return New node that is in place of deleted node. Or null if element for - * delete was not found. - * 注意,删除方法返回的是删除后接管删除节点的位置的节点,返回 - */ - protected Node delete(Node deleteNode) { - if (deleteNode != null) { - Node nodeToReturn = null; - if (deleteNode != null) { - if (deleteNode.left == null) { - // 左孩子为空,右孩子直接替换该节点,达到删除的效果 - // transplant(a,b) b去替换a的环境,a断连掉,把b返回 - nodeToReturn = transplant(deleteNode, deleteNode.right); - } else if (deleteNode.right == null) { - // 右孩子为空,左孩子直接替换,达到删除的目的 - nodeToReturn = transplant(deleteNode, deleteNode.left); - } else { - // 否则,要删除的节点既有左孩子,又有右孩子,找右子树的最左的孩子 - Node successorNode = getMinimum(deleteNode.right); - // 要删除的节点的右孩子,有左孩子。最左孩子的右孩子要它父亲来接管 - if (successorNode.parent != deleteNode) { - transplant(successorNode, successorNode.right); - successorNode.right = deleteNode.right; - successorNode.right.parent = successorNode; - } - // 如果要删除的节点的右孩子,没有左孩子。直接用要删除的节点的右孩子进行替换即可 - transplant(deleteNode, successorNode); - successorNode.left = deleteNode.left; - successorNode.left.parent = successorNode; - nodeToReturn = successorNode; - } - size--; - } - return nodeToReturn; +// Search 在二叉搜索树中寻找element是否存在,如果不存在返回nil +func (tree *BinarySearchTree) Search(element int) *Node { + node := tree.Root + for node != nil && node.Value != element { + if element < node.Value { // 小于当前节点,找左孩子对比 + node = node.Left + } else { // 大于当前节点,找右孩子对比 + node = node.Right } - return null; - } - - /** - * Put one node from tree (newNode) to the place of another (nodeToReplace). - * - * @param nodeToReplace - * Node which is replaced by newNode and removed from tree. - * @param newNode - * New node. - * - * @return New replaced node. - */ - private Node transplant(Node nodeToReplace, Node newNode) { - if (nodeToReplace.parent == null) { - this.root = newNode; - } else if (nodeToReplace == nodeToReplace.parent.left) { - nodeToReplace.parent.left = newNode; - } else { - nodeToReplace.parent.right = newNode; - } - if (newNode != null) { - newNode.parent = nodeToReplace.parent; - } - return newNode; } + return node +} - /** - * @param element - * @return true if tree contains element. - */ - public boolean contains(int element) { - return search(element) != null; +// Insert 向二叉搜索树中插入一个节点 +func (tree *BinarySearchTree) Insert(element int) *Node { + // 首先如果这个树是空的,把该节点当成头节点 + if tree.Root == nil { + tree.Root = createNode(element, nil, nil, nil) + tree.Size++ + return tree.Root } - /** - * @return Minimum element in tree. - */ - public int getMinimum() { - return getMinimum(root).value; - } + // 需要插入在该节点下面。经过上面的base case,这里tree.root一定不为nil + searchTempNode := tree.Root + insertParentNode := searchTempNode - /** - * @return Maximum element in tree. - */ - public int getMaximum() { - return getMaximum(root).value; + for searchTempNode != nil { + insertParentNode = searchTempNode + if element < searchTempNode.Value { + searchTempNode = searchTempNode.Left + } else { + searchTempNode = searchTempNode.Right + } } - /** - * Get next element element who is bigger than provided element. - * - * @param element - * Element for whom descendand element is searched - * @return Successor value. - */ - // TODO Predecessor - public int getSuccessor(int element) { - return getSuccessor(search(element)).value; + newNode := createNode(element, insertParentNode, nil, nil) + if insertParentNode.Value > newNode.Value { + insertParentNode.Left = newNode + } else { + insertParentNode.Right = newNode } + tree.Size++ + return newNode +} - /** - * @return Number of elements in the tree. - */ - public int getSize() { - return size; +// delete 删除二叉搜索树中某个值的对应的节点。删除节点,每个节点由于加入向上的指针,那么旋转的时候会方便些 +func (tree *BinarySearchTree) delete(element int) *Node { + deleteNode := tree.Search(element) + if deleteNode != nil { + return tree.deleteByNode(deleteNode) + } else { + return nil } +} - /** - * Tree traversal with printing element values. In order method. - */ - public void printTreeInOrder() { - printTreeInOrder(root); +// deleteNode 删除二叉搜索树中指定的某个节点。注意,删除方法返回的是删除后接管删除节点的位置的节点,返回 +func (tree *BinarySearchTree) deleteByNode(deleteNode *Node) *Node { + var nodeToReturn *Node + + if deleteNode != nil { + if deleteNode.Left == nil { + // 左孩子为空,右孩子直接替换该节点,达到删除的效果 + // transplant(a,b) b去替换a的环境,a断连掉,把b返回 + nodeToReturn = tree.transplant(deleteNode, deleteNode.Right) + } else if deleteNode.Right == nil { + // 右孩子为空,左孩子直接替换,达到删除的目的 + nodeToReturn = tree.transplant(deleteNode, deleteNode.Left) + } else { + // 否则,要删除的节点既有左孩子,又有右孩子,找右子树的最左的孩子 + successorNode := tree.getMinimumNode(deleteNode.Right) + // 要删除的节点的右孩子,有左孩子。最左孩子的右孩子要它父亲来接管 + if successorNode.Parent != deleteNode { + tree.transplant(successorNode, successorNode.Right) + successorNode.Right = deleteNode.Right + successorNode.Right.Parent = successorNode + } + // 如果要删除的节点的右孩子,没有左孩子。直接用要删除的节点的右孩子进行替换即可 + tree.transplant(deleteNode, successorNode) + successorNode.Left = deleteNode.Left + successorNode.Left.Parent = successorNode + nodeToReturn = successorNode + } + tree.Size-- } + return nodeToReturn +} - /** - * Tree traversal with printing element values. Pre order method. - */ - public void printTreePreOrder() { - printTreePreOrder(root); +// transplant 将树上的一个节点(newNode)放到另一个节点(nodeToReplace)的位置。 +func (tree *BinarySearchTree) transplant(nodeToReplace *Node, newNode *Node) *Node { + if nodeToReplace.Parent == nil { + tree.Root = newNode + } else if nodeToReplace == nodeToReplace.Parent.Left { // nodeToReplace是其父亲的左孩子 + nodeToReplace.Parent.Left = newNode + } else { + nodeToReplace.Parent.Right = newNode } - /** - * Tree traversal with printing element values. Post order method. - */ - public void printTreePostOrder() { - printTreePostOrder(root); + if newNode != nil { + newNode.Parent = nodeToReplace.Parent } + return newNode +} - /*-------------------PRIVATE HELPER METHODS-------------------*/ +// getMinimumValue 查找二叉搜索树中最小的值 +func (tree *BinarySearchTree) getMinimumValue() int { + return tree.getMinimumNode(tree.Root).Value +} - private void printTreeInOrder(Node entry) { - if (entry != null) { - printTreeInOrder(entry.left); - if (entry.value != null) { - System.out.println(entry.value); - } - printTreeInOrder(entry.right); - } +// getMinimumNode 查找二叉搜索树中最小值所在的节点 +func (tree *BinarySearchTree) getMinimumNode(node *Node) *Node { + for node.Left != nil { + node = node.Left } + return node +} - private void printTreePreOrder(Node entry) { - if (entry != null) { - if (entry.value != null) { - System.out.println(entry.value); - } - printTreeInOrder(entry.left); - printTreeInOrder(entry.right); - } - } +func (tree *BinarySearchTree) getMaximumValue() int { + return tree.getMaximumNode(tree.Root).Value +} - private void printTreePostOrder(Node entry) { - if (entry != null) { - printTreeInOrder(entry.left); - printTreeInOrder(entry.right); - if (entry.value != null) { - System.out.println(entry.value); - } - } +func (tree *BinarySearchTree) getMaximumNode(node *Node) *Node { + for node.Right != nil { + node = node.Right } + return node +} - protected Node getMinimum(Node node) { - while (node.left != null) { - node = node.left; - } - return node; - } +// contains 判断二叉搜索树中存不存在element +func (tree *BinarySearchTree) contains(element int) bool { + return tree.Search(element) != nil +} - protected Node getMaximum(Node node) { - while (node.right != null) { - node = node.right; - } - return node; - } - - protected Node getSuccessor(Node node) { - // if there is right branch, then successor is leftmost node of that - // subtree - if (node.right != null) { - return getMinimum(node.right); - } else { // otherwise it is a lowest ancestor whose left child is also - // ancestor of node - Node currentNode = node; - Node parentNode = node.parent; - while (parentNode != null && currentNode == parentNode.right) { - // go up until we find parent that currentNode is not in right - // subtree. - currentNode = parentNode; - parentNode = parentNode.parent; - } - return parentNode; - } +// getSuccessor 获取下一个比提供的元素大的元素值。 +func (tree *BinarySearchTree) getSuccessorValue(element int) int { + return tree.getSuccessorNode(tree.Search(element)).Value +} +// getSuccessorNode 获取下一个比提供的元素大的元素节点。 +func (tree *BinarySearchTree) getSuccessorNode(node *Node) *Node { + // if there is right branch, then successor is leftmost node of that + // subtree + if node.Right != nil { + return tree.getMinimumNode(node.Right) + } else { // otherwise it is a lowest ancestor whose left child is also + // ancestor of node + curNode := node + parentNode := node.Parent + for parentNode != nil && curNode == parentNode.Right { + // go up until we find parent that currentNode is not in right + // subtree. + curNode = parentNode + parentNode = parentNode.Parent + } + return parentNode } +} - // -------------------------------- TREE PRINTING - // ------------------------------------ +// printTreeInOrder 中序遍历 +func (tree *BinarySearchTree) printTreeInOrder() { + printInOrder(tree.Root) +} - public void printTree() { - printSubtree(root); - } +// printTreePreOrder 先序遍历 +func (tree *BinarySearchTree) printTreePreOrder() { + printPreOrder(tree.Root) +} - public void printSubtree(Node node) { - if (node.right != null) { - printTree(node.right, true, ""); - } - printNodeValue(node); - if (node.left != null) { - printTree(node.left, false, ""); - } - } +// printTreePostOrder 后序遍历 +func (tree *BinarySearchTree) printTreePostOrder() { + printPostOrder(tree.Root) +} - private void printNodeValue(Node node) { - if (node.value == null) { - System.out.print(""); - } else { - System.out.print(node.value.toString()); - } - System.out.println(); +func printInOrder(head *Node) { + if head != nil { + printInOrder(head.Left) + fmt.Println(head.Value) + printInOrder(head.Right) } +} - private void printTree(Node node, boolean isRight, String indent) { - if (node.right != null) { - printTree(node.right, true, indent + (isRight ? " " : " | ")); - } - System.out.print(indent); - if (isRight) { - System.out.print(" /"); - } else { - System.out.print(" \\"); - } - System.out.print("----- "); - printNodeValue(node); - if (node.left != null) { - printTree(node.left, false, indent + (isRight ? " | " : " ")); - } +func printPreOrder(head *Node) { + if head != nil { + fmt.Println(head.Value) + printInOrder(head.Left) + printInOrder(head.Right) } +} - public static class Node { - public Node(Integer value, Node parent, Node left, Node right) { - super(); - this.value = value; - this.parent = parent; - this.left = left; - this.right = right; - } - - public Integer value; - public Node parent; - public Node left; - public Node right; - - public boolean isLeaf() { - return left == null && right == null; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((value == null) ? 0 : value.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Node other = (Node) obj; - if (value == null) { - if (other.value != null) - return false; - } else if (!value.equals(other.value)) - return false; - return true; - } - +func printPostOrder(head *Node) { + if head != nil { + printInOrder(head.Left) + printInOrder(head.Right) + fmt.Println(head.Value) } } - ``` @@ -500,7 +308,7 @@ public class AbstractBinarySearchTree { 2)数据状况很差时,性能就很差 -==给搜索二叉树引入两个动作:左旋、右旋== +> 给搜索二叉树引入两个动作:左旋、右旋 输入状况,决定性能。比如输入状况构建出来的树,严重不平衡。极端情况是只有一条通往底部的路径,高度为n; @@ -511,7 +319,7 @@ public class AbstractBinarySearchTree { ### 1.3.1 平衡搜索二叉树 -平衡搜索二叉树,就是在插入和删除的过程中,动态的保持平衡性。保持平衡的代价保持在logN。平衡搜索二叉树的实现由很多,红黑树只是其中一种 +平衡搜索二叉树,就是在插入和删除的过程中,动态的保持平衡性。保持平衡的代价保持在logN。平衡搜索二叉树的实现有很多,红黑树只是其中一种 ### 1.3.2 左旋和右旋 @@ -558,88 +366,64 @@ b-->g 带左旋和右旋的搜索二叉树,在经典搜索二叉树上做的扩展,继承经典搜索二叉树。 -```Java -package class05; - -/** - * Not implemented by zuochengyun - * - * Abstract class for self balancing binary search trees. Contains some methods - * that is used for self balancing trees. - * - * @author Ignas Lelys - * @created Jul 24, 2011 - * - */ -public abstract class AbstractSelfBalancingBinarySearchTree -extends AbstractBinarySearchTree { - - /** - * Rotate to the left. - * - * @param node Node on which to rotate. - * @return Node that is in place of provided node after rotation. - */ - protected Node rotateLeft(Node node) { - Node temp = node.right; - temp.parent = node.parent; - - node.right = temp.left; - if (node.right != null) { - node.right.parent = node; - } - - temp.left = node; - node.parent = temp; - - // temp took over node's place so now its parent should point to temp - if (temp.parent != null) { - if (node == temp.parent.left) { - temp.parent.left = temp; - } else { - temp.parent.right = temp; - } - } else { - root = temp; - } - - return temp; - } - - /** - * Rotate to the right. - * - * @param node Node on which to rotate. - * @return Node that is in place of provided node after rotation. - */ - protected Node rotateRight(Node node) { - Node temp = node.left; - temp.parent = node.parent; - - node.left = temp.right; - if (node.left != null) { - node.left.parent = node; - } - - temp.right = node; - node.parent = temp; - - // temp took over node's place so now its parent should point to temp - if (temp.parent != null) { - if (node == temp.parent.left) { - temp.parent.left = temp; - } else { - temp.parent.right = temp; - } - } else { - root = temp; - } - - return temp; - } +```Go +package main + +// BalancingBinarySearchTree 平衡二叉搜索树,继承二叉搜索树 +type BalancingBinarySearchTree struct { + *BinarySearchTree +} + +// rotateLeft 平衡二叉搜索树的左旋操作 +func (tree *BalancingBinarySearchTree) rotateLeft(node *Node) *Node { + temp := node.Right + temp.Parent = node.Parent + + node.Right = temp.Left + if node.Right != nil { + node.Right.Parent = node + } + + temp.Left = node + node.Parent = temp + if temp.Parent != nil { + if node == temp.Parent.Left { + temp.Parent.Left = temp + } else { + temp.Parent.Right = temp + } + } else { + tree.Root = temp + } + return temp } +// rotateRight 平衡二叉树的右旋操作 +func (tree *BalancingBinarySearchTree) rotateRight(node *Node) *Node { + temp := node.Left + temp.Parent = node.Parent + + node.Left = temp.Right + if node.Left != nil { + node.Left.Parent = node + } + + temp.Right = node + node.Parent = temp + + // temp took over node's place so now its parent should point to temp + if temp.Parent != nil { + if node == temp.Parent.Left { + temp.Parent.Left = temp + } else { + temp.Parent.Right = temp + } + } else { + tree.Root = temp + } + return temp +} ``` @@ -665,7 +449,7 @@ extends AbstractBinarySearchTree { 在有序表中,有序表是一种规范,类似于接口名。规范为key要按序组织,所有操作要是O(logN)等。各种树结构可以实现有序表的功能。其中红黑树只是有序表的一个实现 -==AVL树,SB树,红黑树,都是有序表的一种实现。都是平衡搜索二叉树,这三种树在功能和时间复杂度上几乎无差别,在实现细节上也就是在常数时间上,会有差别。三种树调整检查哪些节点的平衡性相同,下文进行说明。每种树针对每个节点的平衡性调整不同,但是都是使用左旋和右旋两个动作== +**AVL树,SB树,红黑树,都是有序表的一种实现。都是平衡搜索二叉树,这三种树在功能和时间复杂度上几乎无差别,在实现细节上也就是在常数时间上,会有差别。三种树调整检查哪些节点的平衡性相同,下文进行说明。每种树针对每个节点的平衡性调整不同,但是都是使用左旋和右旋两个动作** - AVL树,SB树,红黑树的共性 @@ -688,16 +472,13 @@ AVL树最严格、SB树稍宽松、红黑树最宽松 2)插入、删除和搜索二叉树一样,但是额外,做各自的平衡性调整。各自的平衡性调整所使用的动作都是左旋或者右旋 - - - ### 1.5.1 AVL树 是这三个平衡搜索二叉树中,平衡性最严格的,左树高度和右树高度的绝对值,严格小于2 AVL树在插入节点的时候,只会向上检查节点的平衡性有没有被破坏。删除节点也一样,只会检查删除的那个节点向上的那条链上的节点有没被破坏。删除的时候,如果被删除的节点没有左右孩子那么直接检查,如果有右孩子,是去检查后继节点原来所在的位置的向上节点的平衡性 -==实质上三种树删除和插入节点,检查哪些节点需要调整平衡都是这样的查找规则,对于删除来说,只有左树和只有右树没影响,如果左右树都存在,是去检查后继节点原来所在的位置向上的平衡性。只是具体到某个节点平衡性的处理上,三种树不一样== +**实质上三种树删除和插入节点,检查哪些节点需要调整平衡都是这样的查找规则,对于删除来说,只有左树和只有右树没影响,如果左右树都存在,是去检查后继节点原来所在的位置向上的平衡性。只是具体到某个节点平衡性的处理上,三种树不一样** #### 1.5.1.1 AVL树针对某个节点的平衡性处理 @@ -775,234 +556,12 @@ y-->z - 同理RL形,也是让超过长度的那一侧底部的孙子节点,来到顶部来。 -==LL形和RR形旋转一次O(1),LR和RL形旋转两次,也是O(1)。那么即使删除或者添加的节点影响的整个向上的链路,整体复杂度也是O(logN)== - - -AVL树继承自带左右旋的平衡搜索二叉树,在此基础上做的扩展,code如下: - -```Java -package class05; - -/** - * Not implemented by zuochengyun - * - * AVL tree implementation. - * - * In computer science, an AVL tree is a self-balancing binary search tree, and - * it was the first such data structure to be invented.[1] In an AVL tree, the - * heights of the two child subtrees of any node differ by at most one. Lookup, - * insertion, and deletion all take O(log n) time in both the average and worst - * cases, where n is the number of nodes in the tree prior to the operation. - * Insertions and deletions may require the tree to be rebalanced by one or more - * tree rotations. - * - * @author Ignas Lelys - * @created Jun 28, 2011 - * - */ -public class AVLTree extends AbstractSelfBalancingBinarySearchTree { - - /** - * @see trees.AbstractBinarySearchTree#insert(int) - * - * AVL tree insert method also balances tree if needed. Additional height - * parameter on node is used to track if one subtree is higher than other - * by more than one, if so AVL tree rotations is performed to regain - * balance of the tree. - */ - @Override - public Node insert(int element) { - Node newNode = super.insert(element); - // 对有影响的节点,顺着parent指针向上做平衡性检查调整 - rebalance((AVLNode) newNode); - return newNode; - } - - /** - * @see trees.AbstractBinarySearchTree#delete(int) - */ - @Override - public Node delete(int element) { - // 先查出来需要删的值,存在的话进行删除 - Node deleteNode = super.search(element); - if (deleteNode != null) { - // 先调用父类,也就是带左右旋的平衡搜索二叉树的删除,把删除的节点后哪个节点接管了被删除的位置,该节点返回 - Node successorNode = super.delete(deleteNode); - // 接管的节点不为空,检查是哪一种替换的方式,左孩子接管or右孩子接管,or后继节点接管,or既没有左孩子有没有右孩子直接删除 - if (successorNode != null) { - // if replaced from getMinimum(deleteNode.right) then come back there and update - // heights - AVLNode minimum = successorNode.right != null ? (AVLNode) getMinimum(successorNode.right) - : (AVLNode) successorNode; - // 重新计算高度(重要) - recomputeHeight(minimum); - // 重新进行平衡(重要) - rebalance((AVLNode) minimum); - } else { // 并没有任何节点替代被删除节点的位置,被删除节点是孤零零被删除的 - recomputeHeight((AVLNode) deleteNode.parent); - rebalance((AVLNode) deleteNode.parent); - } - return successorNode; - } - return null; - } - - /** - * @see trees.AbstractBinarySearchTree#createNode(int, - * trees.AbstractBinarySearchTree.Node, - * trees.AbstractBinarySearchTree.Node, - * trees.AbstractBinarySearchTree.Node) - */ - @Override - protected Node createNode(int value, Node parent, Node left, Node right) { - return new AVLNode(value, parent, left, right); - } - - /** - * Go up from inserted node, and update height and balance informations if - * needed. If some node balance reaches 2 or -2 that means that subtree must be - * rebalanced. - * - * @param node Inserted Node. - */ - private void rebalance(AVLNode node) { - while (node != null) { - - // 先记录一下父环境 - Node parent = node.parent; - - // 左右树的高度拿出来 - int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height; - int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height; - int nodeBalance = rightHeight - leftHeight; - // rebalance (-2 means left subtree outgrow, 2 means right subtree) - // 右树过高 - if (nodeBalance == 2) { - // 判断是RR形还是RL形,确定进行一次还是两次旋转 - if (node.right.right != null) { - // 旋转,旋转的过程中,一定维护好高度信息 - node = (AVLNode) avlRotateLeft(node); - break; - } else { - node = (AVLNode) doubleRotateRightLeft(node); - break; - } - // 左树过高 - } else if (nodeBalance == -2) { - // 同理,判断是LL还是LR - if (node.left.left != null) { - node = (AVLNode) avlRotateRight(node); - break; - } else { - node = (AVLNode) doubleRotateLeftRight(node); - break; - } - } else { - updateHeight(node); - } +**LL形和RR形旋转一次O(1),LR和RL形旋转两次,也是O(1)。那么即使删除或者添加的节点影响的整个向上的链路,整体复杂度也是O(logN)** - // 把当前node向上变为父节点,向上窜,继续调整平衡 - node = (AVLNode) parent; - } - } - /** - * Rotates to left side. - */ - private Node avlRotateLeft(Node node) { - Node temp = super.rotateLeft(node); +AVL树继承自带左右旋的平衡搜索二叉树,在此基础上做的扩展,重写继承的抽象类平衡二叉搜索树的相应方法实现即可。 - updateHeight((AVLNode) temp.left); - updateHeight((AVLNode) temp); - return temp; - } - - /** - * Rotates to right side. - */ - private Node avlRotateRight(Node node) { - Node temp = super.rotateRight(node); - - updateHeight((AVLNode) temp.right); - updateHeight((AVLNode) temp); - return temp; - } - - /** - * Take right child and rotate it to the right side first and then rotate node - * to the left side. - */ - protected Node doubleRotateRightLeft(Node node) { - node.right = avlRotateRight(node.right); - return avlRotateLeft(node); - } - - /** - * Take right child and rotate it to the right side first and then rotate node - * to the left side. - */ - protected Node doubleRotateLeftRight(Node node) { - node.left = avlRotateLeft(node.left); - return avlRotateRight(node); - } - - /** - * Recomputes height information from the node and up for all of parents. It - * needs to be done after delete. - */ - private void recomputeHeight(AVLNode node) { - while (node != null) { - node.height = maxHeight((AVLNode) node.left, (AVLNode) node.right) + 1; - node = (AVLNode) node.parent; - } - } - - /** - * Returns higher height of 2 nodes. - */ - private int maxHeight(AVLNode node1, AVLNode node2) { - if (node1 != null && node2 != null) { - return node1.height > node2.height ? node1.height : node2.height; - } else if (node1 == null) { - return node2 != null ? node2.height : -1; - } else if (node2 == null) { - return node1 != null ? node1.height : -1; - } - return -1; - } - - /** - * Updates height and balance of the node. - * - * @param node Node for which height and balance must be updated. - */ - private static final void updateHeight(AVLNode node) { - int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height; - int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height; - node.height = 1 + Math.max(leftHeight, rightHeight); - } - - /** - * Node of AVL tree has height and balance additional properties. If balance - * equals 2 (or -2) that node needs to be re balanced. (Height is height of the - * subtree starting with this node, and balance is difference between left and - * right nodes heights). - * - * @author Ignas Lelys - * @created Jun 30, 2011 - * AVLNode继承自搜索二叉树的node,额外补充一个高度信息,用这个高度信息做平衡,一个节点的高度,是以该节点做头节点的树的高度 - */ - protected static class AVLNode extends Node { - public int height; - - public AVLNode(int value, Node parent, Node left, Node right) { - super(value, parent, left, right); - } - } - -} - -``` +Avl树的一种实现方式参考:https://github.com/emirpasic/gods/blob/master/trees/avltree/avltree.go ### 1.5.2 SB树 @@ -1049,663 +608,504 @@ e-->g 2、 RR形违规,和LL形违规类似处理; -==SB树平衡性相对于AVL树要模糊,所以平衡性调整比AVL的调整粒度要粗,也意味着SB树比AVL树速度要快,比赛常用。而且SB树可以设计成删除节点的时候,不进行平衡性调整,只有在添加节点的时候再进行平衡性调整,添加节点的时候有可能积压了很多的不平衡,但是我们有递归行为,仍然可以调整回平衡的状态;可能为棒状,有可能该次递归行为时间复杂度比较高,但是均摊下来仍然O(logN)水平;该结构比较重要== +**SB树平衡性相对于AVL树要模糊,所以平衡性调整比AVL的调整粒度要粗,也意味着SB树比AVL树速度要快,比赛常用。而且SB树可以设计成删除节点的时候,不进行平衡性调整,只有在添加节点的时候再进行平衡性调整,添加节点的时候有可能积压了很多的不平衡,但是我们有递归行为,仍然可以调整回平衡的状态;可能为棒状,有可能该次递归行为时间复杂度比较高,但是均摊下来仍然O(logN)水平;该结构比较重要** -==如果是违规的加点的左树高度超了,且左孩子的左右子节点个数相同,必须做LL形的调整,反之RR形同理== +**如果是违规的加点的左树高度超了,且左孩子的左右子节点个数相同,必须做LL形的调整,反之RR形同理** - SB树的树结构版本 -```Java -package class06; - -public class Code01_SizeBalancedTreeMap { - - // k继承Comparable,可比较的泛型 - public static class SBTNode, V> { - // 该节点的Key - public K key; - // 该节点的V - public V value; - // 节点的左孩子 - public SBTNode l; - // 节点的右孩子 - public SBTNode r; - public int size; // 不同的key的数量 - - public SBTNode(K key, V value) { - this.key = key; - this.value = value; - size = 1; - } - } +```Go +package main + +import "fmt" + +// SBTNode SBT树的节点类型 +type SBTNode struct { + // 该节点的Key + key string + // 该节点的V + value int + // 节点的左孩子 + l *SBTNode + // 节点的右孩子 + r *SBTNode + // 不同的key的数量 + size int +} - public static class SizeBalancedTreeMap, V> { - private SBTNode root; - - // 右旋交换节点时,size要互换,维护正确的size信息。返回右旋之后的新头部 - private SBTNode rightRotate(SBTNode cur) { - // 由于右旋,要维护好左子树 - SBTNode leftNode = cur.l; - // 左孩子的右,给当前节点的左 - cur.l = leftNode.r; - // 左孩子的右指向当前节点,画图可以清晰看到就是右旋操作 - leftNode.r = cur; - // 维护size - leftNode.size = cur.size; - cur.size = (cur.l != null ? cur.l.size : 0) + (cur.r != null ? cur.r.size : 0) + 1; - return leftNode; - } +type Comparator func(a, b interface{}) int - // 左旋操作和右旋操作同理 - private SBTNode leftRotate(SBTNode cur) { - SBTNode rightNode = cur.r; - cur.r = rightNode.l; - rightNode.l = cur; - rightNode.size = cur.size; - cur.size = (cur.l != null ? cur.l.size : 0) + (cur.r != null ? cur.r.size : 0) + 1; - return rightNode; - } +// StringComparator 字符串字典序比较器。参考标准库实现,返回0,1,-1。 +func StringComparator(a, b interface{}) int { + s1 := a.(string) + s2 := b.(string) + if s1 == s2 { + return 0 + } + if s1 < s2 { + return -1 + } + return +1 +} - // 调整 - private SBTNode maintain(SBTNode cur) { - if (cur == null) { - return null; - } - // LL形 - if (cur.l != null && cur.l.l != null && cur.r != null && cur.l.l.size > cur.r.size) { - // 当前节点右旋 - cur = rightRotate(cur); - // 递归调整右孩子 - cur.r = maintain(cur.r); - // 递归调用调整当前节点 - cur = maintain(cur); - } else if (cur.l != null && cur.l.r != null && cur.r != null && cur.l.r.size > cur.r.size) { - cur.l = leftRotate(cur.l); - cur = rightRotate(cur); - cur.l = maintain(cur.l); - cur.r = maintain(cur.r); - cur = maintain(cur); - } else if (cur.r != null && cur.r.r != null && cur.l != null && cur.r.r.size > cur.l.size) { - cur = leftRotate(cur); - cur.l = maintain(cur.l); - cur = maintain(cur); - } else if (cur.r != null && cur.r.l != null && cur.l != null && cur.r.l.size > cur.l.size) { - cur.r = rightRotate(cur.r); - cur = leftRotate(cur); - cur.l = maintain(cur.l); - cur.r = maintain(cur.r); - cur = maintain(cur); - } - return cur; - } +type SBTree struct { + Root *SBTNode + Comparator Comparator +} - private SBTNode findLastIndex(K key) { - SBTNode pre = root; - SBTNode cur = root; - while (cur != null) { - pre = cur; - if (key.compareTo(cur.key) == 0) { - break; - } else if (key.compareTo(cur.key) < 0) { - cur = cur.l; - } else { - cur = cur.r; - } - } - return pre; - } +// InitSBTTree 构建一个SBT树,返回头节点 +func InitSBTTree(key string, value int) *SBTree { + root := &SBTNode{ + key: key, + value: value, + size: 1, + } - private SBTNode findLastNoSmallIndex(K key) { - SBTNode ans = null; - SBTNode cur = root; - while (cur != null) { - if (key.compareTo(cur.key) == 0) { - ans = cur; - break; - } else if (key.compareTo(cur.key) < 0) { - ans = cur; - cur = cur.l; - } else { - cur = cur.r; - } - } - return ans; - } + sbTree := &SBTree{ + Root: root, + Comparator: StringComparator, + } + return sbTree +} - private SBTNode findLastNoBigIndex(K key) { - SBTNode ans = null; - SBTNode cur = root; - while (cur != null) { - if (key.compareTo(cur.key) == 0) { - ans = cur; - break; - } else if (key.compareTo(cur.key) < 0) { - cur = cur.l; - } else { - ans = cur; - cur = cur.r; - } - } - return ans; - } +// rightRotate 右旋交换节点时,size要互换,维护正确的size信息。返回右旋之后的新头部 +func (tree *SBTree) rightRotate(cur *SBTNode) *SBTNode { + // 由于右旋,要维护好左子树 + leftNode := cur.l + // 左孩子的右,给当前节点的左 + cur.l = leftNode.r + // 左孩子的右,指向当前节点,画图可以清晰看到就是右旋操作 + leftNode.r = cur + // 维护size + leftNode.size = cur.size + + lSize := 0 + rSize := 0 + if cur.l != nil { + lSize = cur.l.size + } else { + lSize = 0 + } - // 现在,以cur为头的树上,加(key, value)这样的记录 - // 加完之后,会对cur做检查,该调整调整 - // 返回,调整完之后,整棵树的新头部 - private SBTNode add(SBTNode cur, K key, V value) { - if (cur == null) { - return new SBTNode(key, value); - } else { - cur.size++; - if (key.compareTo(cur.key) < 0) { - cur.l = add(cur.l, key, value); - } else { - cur.r = add(cur.r, key, value); - } - return maintain(cur); - } - } + if cur.r != nil { + rSize = cur.r.size + } else { + rSize = 0 + } - // 在cur这棵树上,删掉key所代表的节点 - // 返回cur这棵树的新头部 - private SBTNode delete(SBTNode cur, K key) { - cur.size--; - if (key.compareTo(cur.key) > 0) { - cur.r = delete(cur.r, key); - } else if (key.compareTo(cur.key) < 0) { - cur.l = delete(cur.l, key); - } else { // 当前要删掉cur - if (cur.l == null && cur.r == null) { - // free cur memory -> C++ - cur = null; - } else if (cur.l == null && cur.r != null) { - // free cur memory -> C++ - cur = cur.r; - } else if (cur.l != null && cur.r == null) { - // free cur memory -> C++ - cur = cur.l; - } else { // 有左有右 - SBTNode pre = null; - SBTNode des = cur.r; - des.size--; - while (des.l != null) { - pre = des; - des = des.l; - des.size--; - } - if (pre != null) { - pre.l = des.r; - des.r = cur.r; - } - des.l = cur.l; - des.size = des.l.size + (des.r == null ? 0 : des.r.size) + 1; - // free cur memory -> C++ - cur = des; - } - } - return cur; - } + cur.size = lSize + rSize + 1 + return leftNode +} - private SBTNode getIndex(SBTNode cur, int kth) { - if (kth == (cur.l != null ? cur.l.size : 0) + 1) { - return cur; - } else if (kth <= (cur.l != null ? cur.l.size : 0)) { - return getIndex(cur.l, kth); - } else { - return getIndex(cur.r, kth - (cur.l != null ? cur.l.size : 0) - 1); - } - } +// leftRotate 左旋操作和右旋操作同理 +func (tree *SBTree) leftRotate(cur *SBTNode) *SBTNode { + rightNode := cur.r + cur.r = rightNode.l + rightNode.l = cur + rightNode.size = cur.size + lSize := 0 + rSize := 0 + if cur.l != nil { + lSize = cur.l.size + } else { + lSize = 0 + } - public int size() { - return root == null ? 0 : root.size; - } + if cur.r != nil { + rSize = cur.r.size + } else { + rSize = 0 + } - public boolean containsKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - SBTNode lastNode = findLastIndex(key); - return lastNode != null && key.compareTo(lastNode.key) == 0 ? true : false; - } + cur.size = lSize + rSize + 1 + return rightNode +} - // put方法,有可能是新增有可能是覆盖更新 - public void put(K key, V value) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - // 先查找该key是不是已经存在 - SBTNode lastNode = findLastIndex(key); - if (lastNode != null && key.compareTo(lastNode.key) == 0) { - lastNode.value = value; - } else { - // 不存在的话,从根节点调用递归,加入到合适的位置。sb树由于没有向上指针,这里需要从头结点开始调用递归 - // 添加进去后,有可能需要调整,头部有可能会变,返回新的头部 - root = add(root, key, value); - } - } +// maintain 对传入的cur节点,做平衡性调整。包含四种不平衡的调整策略:LL, LR, RL, RR +func (tree *SBTree) maintain(cur *SBTNode) *SBTNode { + if cur == nil { + return nil + } - public void remove(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - if (containsKey(key)) { - root = delete(root, key); - } - } + if cur.l != nil && cur.l.l != nil && cur.r != nil && cur.l.l.size > cur.r.size { // LL形不平衡调整策略 + // 当前节点右旋 + cur = tree.rightRotate(cur) + // 递归调整个右孩子 + cur.r = tree.maintain(cur.r) + // 递归调用调整当前节点 + cur = tree.maintain(cur) + } else if cur.l != nil && cur.l.r != nil && cur.r != nil && cur.l.r.size > cur.r.size { // LR形不平衡调整策略 + // 当前节点左孩子左旋 + cur.l = tree.leftRotate(cur.l) + // 当前节点右旋 + cur = tree.rightRotate(cur) + // 递归调用调整节点左孩子 + cur.l = tree.maintain(cur.l) + // 递归调用调整节点右孩子 + cur.r = tree.maintain(cur.r) + // 递归调用调整当前节点 + cur = tree.maintain(cur) + } else if cur.r != nil && cur.r.r != nil && cur.l != nil && cur.r.r.size > cur.l.size { // RR形不平衡调整策略 + // 当前节点左旋 + cur = tree.leftRotate(cur) + // 递归调整当前节点左孩子 + cur.l = tree.maintain(cur.l) + // 递归调整当前节点右孩子 + cur = tree.maintain(cur) + } else if cur.r != nil && cur.r.l != nil && cur.l != nil && cur.r.l.size > cur.l.size { // RL形不平衡调整策略 + // 当前节点右孩子右旋 + cur.r = tree.rightRotate(cur.r) + // 当前节点左旋 + cur = tree.leftRotate(cur) + // 递归调整当前节点左孩子 + cur.l = tree.maintain(cur.l) + // 递归调整当前节点右孩子 + cur.r = tree.maintain(cur.r) + // 递归调用调整当前节点 + cur = tree.maintain(cur) + } + return cur +} - public K getIndexKey(int index) { - if (index < 0 || index >= this.size()) { - throw new RuntimeException("invalid parameter."); - } - return getIndex(root, index + 1).key; +// findLastIndex 查找以key为节点key的节点是否存在 +func (tree *SBTree) findLastIndex(key string) *SBTNode { + pre := tree.Root + cur := tree.Root + for cur != nil { + pre = cur + if tree.Comparator(key, cur.key) == 0 { + break + } else if tree.Comparator(key, cur.key) < 0 { + cur = cur.l + } else { + cur = cur.r } + } + return pre +} - public V getIndexValue(int index) { - if (index < 0 || index >= this.size()) { - throw new RuntimeException("invalid parameter."); - } - return getIndex(root, index + 1).value; +// findLastNoSmallIndex 找到第一个不比key小的节点,返回。即搜索树中字典序大于key的第一个节点 +func (tree *SBTree) findLastNoSmallIndex(key string) *SBTNode { + var ans *SBTNode + cur := tree.Root + for cur != nil { + if tree.Comparator(key, cur.key) == 0 { + ans = cur + break + } else if tree.Comparator(key, cur.key) < 0 { + ans = cur + cur = cur.l + } else { + cur = cur.r } + } + return ans +} - public V get(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - SBTNode lastNode = findLastIndex(key); - if (lastNode != null && key.compareTo(lastNode.key) == 0) { - return lastNode.value; - } else { - return null; - } +// findLastNoBigIndex 在搜索树上查找不大于key的第一个数,即小于等于key的第一个节点返回 +func (tree *SBTree) findLastNoBigIndex(key string) *SBTNode { + var ans *SBTNode + cur := tree.Root + for cur != nil { + if tree.Comparator(key, cur.key) == 0 { + ans = cur + break + } else if tree.Comparator(key, cur.key) < 0 { + cur = cur.l + } else { + ans = cur + cur = cur.r } + } + return ans +} - public K firstKey() { - if (root == null) { - return null; - } - SBTNode cur = root; - while (cur.l != null) { - cur = cur.l; - } - return cur.key; +// add tree树上,加(key, value)这样的一条记录。cur传入tree的头节点 +// 加完之后,会对tree做检查,该调整调整 +// 返回,调整完之后,整棵树的新头部, 被替换掉了。把新树头节点返回 +func (tree *SBTree) add(cur *SBTNode, key string, value int) *SBTNode { + if cur == nil { + return InitSBTTree(key, value).Root + } else { + cur.size++ + if tree.Comparator(key, cur.key) < 0 { + cur.l = tree.add(cur.l, key, value) + } else { + cur.r = tree.add(cur.r, key, value) } + return tree.maintain(cur) + } +} - public K lastKey() { - if (root == null) { - return null; - } - SBTNode cur = root; - while (cur.r != null) { - cur = cur.r; +// 在cur这棵树上,删掉key所代表的节点。cur为tree的头节点 +// 返回cur这棵树的新头部 +func (tree *SBTree) delete(cur *SBTNode, key string) *SBTNode { + cur.size-- + if tree.Comparator(key, cur.key) > 0 { + cur.r = tree.delete(cur.r, key) + } else if tree.Comparator(key, cur.key) < 0 { + cur.l = tree.delete(cur.l, key) + } else { // 当前要删掉cur + if cur.l == nil && cur.r == nil { + cur = nil // free cur memory + } else if cur.l == nil && cur.r != nil { + cur = cur.r // free cur memory + } else if cur.l != nil && cur.r == nil { + cur = cur.l // free cur memory + } else { // 有左右右的情况 + var pre *SBTNode + des := cur.r + des.size-- + + for des.l != nil { + pre = des + des = des.l + des.size-- + } + if pre != nil { + pre.l = des.r + des.r = cur.r + } + + des.l = cur.l + desRSize := 0 + if des.r == nil { + desRSize = 0 + } else { + desRSize = des.r.size } - return cur.key; - } - public K floorKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - SBTNode lastNoBigNode = findLastNoBigIndex(key); - return lastNoBigNode == null ? null : lastNoBigNode.key; + des.size = des.l.size + desRSize + 1 + // free cur memory + cur = des } + } + return cur +} - public K ceilingKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - SBTNode lastNoSmallNode = findLastNoSmallIndex(key); - return lastNoSmallNode == null ? null : lastNoSmallNode.key; - } +func (tree *SBTree) getIndex(cur *SBTNode, kth int) *SBTNode { + lSize := 0 + if cur.l != nil { + lSize = cur.l.size + } else { + lSize = 0 + } + if kth == lSize+1 { + return cur + } else if kth <= lSize { + return tree.getIndex(cur.l, kth) + } else { + return tree.getIndex(cur.r, kth-lSize-1) } +} - // for test - public static void printAll(SBTNode head) { - System.out.println("Binary Tree:"); - printInOrder(head, 0, "H", 17); - System.out.println(); +func (tree *SBTree) size() int { + if tree.Root == nil { + return 0 + } else { + return tree.Root.size } +} - // for test - public static void printInOrder(SBTNode head, int height, String to, int len) { - if (head == null) { - return; - } - printInOrder(head.r, height + 1, "v", len); - String val = to + "(" + head.key + "," + head.value + ")" + to; - int lenM = val.length(); - int lenL = (len - lenM) / 2; - int lenR = len - lenM - lenL; - val = getSpace(lenL) + val + getSpace(lenR); - System.out.println(getSpace(height * len) + val); - printInOrder(head.l, height + 1, "^", len); - } - - // for test - public static String getSpace(int num) { - String space = " "; - StringBuffer buf = new StringBuffer(""); - for (int i = 0; i < num; i++) { - buf.append(space); - } - return buf.toString(); - } - - public static void main(String[] args) { - SizeBalancedTreeMap sbt = new SizeBalancedTreeMap(); - sbt.put("d", 4); - sbt.put("c", 3); - sbt.put("a", 1); - sbt.put("b", 2); - // sbt.put("e", 5); - sbt.put("g", 7); - sbt.put("f", 6); - sbt.put("h", 8); - sbt.put("i", 9); - sbt.put("a", 111); - System.out.println(sbt.get("a")); - sbt.put("a", 1); - System.out.println(sbt.get("a")); - for (int i = 0; i < sbt.size(); i++) { - System.out.println(sbt.getIndexKey(i) + " , " + sbt.getIndexValue(i)); - } - printAll(sbt.root); - System.out.println(sbt.firstKey()); - System.out.println(sbt.lastKey()); - System.out.println(sbt.floorKey("g")); - System.out.println(sbt.ceilingKey("g")); - System.out.println(sbt.floorKey("e")); - System.out.println(sbt.ceilingKey("e")); - System.out.println(sbt.floorKey("")); - System.out.println(sbt.ceilingKey("")); - System.out.println(sbt.floorKey("j")); - System.out.println(sbt.ceilingKey("j")); - sbt.remove("d"); - printAll(sbt.root); - sbt.remove("f"); - printAll(sbt.root); +func (tree *SBTree) containsKey(key string) (bool, error) { + if len(key) == 0 { + return false, fmt.Errorf("invalid parameter") + } + lastNode := tree.findLastIndex(key) + isEqual := false + if tree.Comparator(key, lastNode.key) == 0 { + isEqual = true + } else { + isEqual = false } + return lastNode != nil && isEqual, nil } -``` - -- SB树的数组版本 - -```Java -package class06; - -import java.util.ArrayList; - -public class Code02_SizeBalancedTreeMap { - - public static class SizeBalancedTreeMap, V> { - private int root; - private int len; - private int[] left; - private int[] right; - private int[] size; - private ArrayList keys; - private ArrayList values; - - public SizeBalancedTreeMap(int init) { - left = new int[init + 1]; - right = new int[init + 1]; - size = new int[init + 1]; - keys = new ArrayList(); - values = new ArrayList(); - keys.add(null); - values.add(null); - root = 0; - len = 0; - } - private int rightRotate(int index) { - int iLeft = left[index]; - left[index] = right[iLeft]; - right[iLeft] = index; - size[iLeft] = size[index]; - size[index] = size[left[index]] + size[right[index]] + 1; - return iLeft; - } +// put 方法,有可能是新增有可能是覆盖更新 +func (tree *SBTree) put(key string, value int) error { + if len(key) == 0 { + return fmt.Errorf("invalid parameter") + } - private int leftRotate(int index) { - int iRight = right[index]; - right[index] = left[iRight]; - left[iRight] = index; - size[iRight] = size[index]; - size[index] = size[left[index]] + size[right[index]] + 1; - return iRight; - } + lastNode := tree.findLastIndex(key) + if lastNode != nil && tree.Comparator(key, lastNode.key) == 0 { + lastNode.value = value + } else { + // 不存在的话,从根节点调用递归,加入到合适的位置。sb树由于没有向上指针,这里需要从头结点开始调用递归 + // 添加进去后,有可能需要调整,头部有可能会变,返回新的头部 + tree.Root = tree.add(tree.Root, key, value) + } + return nil +} - private int matain(int index) { - if (size[left[left[index]]] > size[right[index]]) { - index = rightRotate(index); - right[index] = matain(right[index]); - index = matain(index); - } else if (size[right[left[index]]] > size[right[index]]) { - left[index] = leftRotate(left[index]); - index = rightRotate(index); - left[index] = matain(left[index]); - right[index] = matain(right[index]); - index = matain(index); - } else if (size[right[right[index]]] > size[left[index]]) { - index = leftRotate(index); - left[index] = matain(left[index]); - index = matain(index); - } else if (size[left[right[index]]] > size[left[index]]) { - right[index] = rightRotate(right[index]); - index = leftRotate(index); - left[index] = matain(left[index]); - right[index] = matain(right[index]); - index = matain(index); - } - return index; - } +func (tree *SBTree) remove(key string) error { + if len(key) == 0 { + return fmt.Errorf("invalid parameter") + } - private int findLastIndex(K key) { - int pre = root; - int cur = root; - while (cur != 0) { - pre = cur; - if (key.compareTo(keys.get(cur)) == 0) { - break; - } else if (key.compareTo(keys.get(cur)) < 0) { - cur = left[cur]; - } else { - cur = right[cur]; - } - } - return pre; - } + if b, e := tree.containsKey(key); e == nil && b { + tree.Root = tree.delete(tree.Root, key) + } + return nil +} - private int findLastNoSmallIndex(K key) { - int ans = 0; - int cur = root; - while (cur != 0) { - if (key.compareTo(keys.get(cur)) == 0) { - ans = cur; - break; - } else if (key.compareTo(keys.get(cur)) < 0) { - ans = cur; - cur = left[cur]; - } else { - cur = right[cur]; - } - } - return ans; - } +func (tree *SBTree) getIndexKey(index int) (string, error) { + if index < 0 || index > tree.Root.size { + return "", fmt.Errorf("invalid parameter") + } - private int findLastNoBigIndex(K key) { - int ans = 0; - int cur = root; - while (cur != 0) { - if (key.compareTo(keys.get(cur)) == 0) { - ans = cur; - break; - } else if (key.compareTo(keys.get(cur)) < 0) { - cur = left[cur]; - } else { - ans = cur; - cur = right[cur]; - } - } - return ans; - } + return tree.getIndex(tree.Root, index + 1).key, nil +} - private int add(int index, K key, V value) { - if (index == 0) { - index = ++len; - keys.add(key); - values.add(value); - size[index] = 1; - left[index] = 0; - right[index] = 0; - return index; - } else { - size[index]++; - if (key.compareTo(keys.get(index)) < 0) { - left[index] = add(left[index], key, value); - } else { - right[index] = add(right[index], key, value); - } - return matain(index); - } - } +func (tree *SBTree) getIndexValue(index int) (int, error) { + if index < 0 || index > tree.Root.size { + return -1, fmt.Errorf("invalid parameter") + } - private int getIndex(int index, int kth) { - if (kth == size[left[index]] + 1) { - return index; - } else if (kth <= size[left[index]]) { - return getIndex(left[index], kth); - } else { - return getIndex(right[index], kth - size[left[index]] - 1); - } - } + return tree.getIndex(tree.Root, index + 1).value, nil +} - public int size() { - return len; - } +func (tree *SBTree) get(key string) (int, error) { + if len(key) == 0 { + return -1, fmt.Errorf("invalid parameter") + } - public boolean containsKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - int lastIndex = findLastIndex(key); - return lastIndex != 0 && key.compareTo(keys.get(lastIndex)) == 0 ? true : false; - } + lastNode := tree.findLastIndex(key) + if lastNode != nil && tree.Comparator(key, lastNode.key) == 0 { + return lastNode.value, nil + } else { + return -1, fmt.Errorf("not find") + } +} - public void put(K key, V value) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - if (len == size.length - 1) { - throw new RuntimeException("size balanced tree is full."); - } - int lastIndex = findLastIndex(key); - if (lastIndex != 0 && key.compareTo(keys.get(lastIndex)) == 0) { - values.set(lastIndex, value); - } else { - root = add(root, key, value); - } - } +func (tree *SBTree) firstKey() (string, error) { + if tree.Root == nil { + return "", fmt.Errorf("not find because root is nil") + } - public K getIndexKey(int index) { - if (index < 0 || index >= len) { - throw new RuntimeException("invalid parameter."); - } - return keys.get(getIndex(root, index + 1)); - } + cur := tree.Root + for cur.l != nil { + cur = cur.l + } - public V getIndexValue(int index) { - if (index < 0 || index >= len) { - throw new RuntimeException("invalid parameter."); - } - return values.get(getIndex(root, index + 1)); - } + return cur.key, nil +} - public V get(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - int lastIndex = findLastIndex(key); - if (lastIndex != 0 && key.compareTo(keys.get(lastIndex)) == 0) { - return values.get(lastIndex); - } else { - return null; - } - } +func (tree *SBTree) lastKey() (string, error) { + if tree.Root == nil { + return "", fmt.Errorf("not find because root is nil") + } - public K firstKey() { - int cur = root; - while (left[cur] != 0) { - cur = left[cur]; - } - return cur == 0 ? null : keys.get(cur); - } + cur := tree.Root + for cur.r != nil { + cur = cur.r + } + return cur.key, nil +} - public K lastKey() { - int cur = root; - while (right[cur] != 0) { - cur = right[cur]; - } - return cur == 0 ? null : keys.get(cur); - } +func (tree *SBTree) floorKey(key string) (string, error) { + if len(key) == 0 { + return "", fmt.Errorf("invalid parameter") + } - public K floorKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - int lastNoBigIndex = findLastNoBigIndex(key); - return lastNoBigIndex == 0 ? null : keys.get(lastNoBigIndex); - } + lastNoBigNode := tree.findLastNoBigIndex(key) + if lastNoBigNode == nil { + return "", fmt.Errorf("not find") + } else { + return lastNoBigNode.key, nil + } +} - public K ceilingKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - int lastNoSmallIndex = findLastNoSmallIndex(key); - return lastNoSmallIndex == 0 ? null : keys.get(lastNoSmallIndex); - } +func (tree *SBTree) ceilingKey(key string) (string, error) { + if len(key) == 0 { + return "", fmt.Errorf("invalid parameter") + } + lastNoSmallNode := tree.findLastNoSmallIndex(key) + if lastNoSmallNode == nil { + return "", fmt.Errorf("not find") + } else { + return lastNoSmallNode.key, nil } +} - public static void main(String[] args) { - SizeBalancedTreeMap sbt = new SizeBalancedTreeMap(10000); +func main() { + sbt := InitSBTTree("d", 4) + sbt.put("c", 3) + sbt.put("a", 1) + sbt.put("b", 2) + // sbt.put("e", 5); + sbt.put("g", 7) + sbt.put("f", 6) + sbt.put("h", 8) + sbt.put("i", 9) + sbt.put("a", 111) + fmt.Println(sbt.get("a")) + sbt.put("a", 1) + fmt.Println(sbt.get("a")) + for i := 0; i 红黑树的实现,可以参考:https://github.com/emirpasic/gods/blob/master/trees/redblacktree/redblacktree.go + ##### Redis为什么选择跳表的结构? Redis为什么选择跳表的结构,而不是AVL和SB树呢,实质上可以选择,但是考虑到redis可能需要对有序表进行序列化的要求,SkipList就是多层的线性结构,比较好序列化。AVL和SB是个结构化的东西,不好序列化;一种技术的选型,需要根据自己的生存状态去选择的 +**三种树的平衡性保证策略不同,各自实现各自的平衡性,但是三个树都只有左旋和右旋两种调整策略** + +## 1.6 跳表SkipList(也可实现有序表功能) -==三种树的平衡性保证策略不同,各自实现各自的平衡性,但是三个树都只有左旋和右旋两种调整策略== +**最烧脑的结构** +跳表也可实现有序表的功能,但是跳表不是搜索二叉树,实现机制跟二叉树也没关系 +**跳表实现有序表,比较好实现,思想也相对更先进O(logN)** -## 1.6 跳表SkipList(也可实现有序表功能) +跳表节点有多条往外指的指针,Node里面有一个List变量,类似于多叉树;跳表节点上也有可以比较的key,定义最小的key是null +每个节点有多少个向下指针,随机指定,高层一定和所有节点最多的向下指针的条目数保持一致 -==最烧脑的结构== +跳表的最低层一定含有所有记录节点,概率上第二层有N/2个节点,概率上第三层会有N/4个节点... -跳表也可实现有序表的功能,但是跳表不是搜索二叉树,实现机制跟二叉树也没关系 +高层向底层寻找,实际上跳跃了很多的节点。这种机制跟输入的数据状况没关系,每一个节点随机层数,最后查找复杂度为O(logN) -==跳表实现有序表,比较好实现,思想也相对更先进O(logN)== +```Go +package main +import ( + "fmt" + "math" + "math/rand" + "time" +) -跳表节点有多条往外指的指针,Node里面有一个List变量,类似于多叉树;跳表节点上也有可以比较的key,定义最小的key是null +const ( + UP_LEVELS_ABILITY = 500 + UP_LEVELS_TOTAL = 1000 +) +type skipListNode struct { + score int64 + val interface{} + next *skipListNode + pre *skipListNode + up *skipListNode + down *skipListNode +} +type skipList struct { + head *skipListNode + tail *skipListNode + size int + levels int +} -每个节点有多少个向下指针,随机指定,高层一定和所有节点最多的向下指针的条目数保持一致 +func NewSkipList() *skipList { + sl := new(skipList) + sl.head = new(skipListNode) + sl.tail = new(skipListNode) + sl.head.score = math.MinInt64 + sl.tail.score = math.MaxInt64 + sl.head.next = sl.tail + sl.tail.pre = sl.head -跳表的最低层一定含有所有记录节点,概率上第二层有N/2个节点,概率上第三层会有N/4个节点... + sl.size = 0 + sl.levels = 1 -高层向底层寻找,实际上跳跃了很多的节点。这种机制跟输入的数据状况没关系,每一个节点随机层数,最后查找O(logN) + return sl +} +func (sl *skipList) Size() int { + return sl.size +} +func (sl *skipList) Levels() int { + return sl.levels +} -```Java -package class06; +func (sl *skipList) Get(score int64) interface{} { + node := sl.findNode(score) + if node.score == score { + return node.val + } else { + return nil + } +} -import java.util.ArrayList; +func (sl *skipList) Insert(score int64, val interface{}) { + f := sl.findNode(score) + if f.score == score { + f.val = val + return + } + curNode := new(skipListNode) + curNode.score = score + curNode.val = val -public class Code03_SkipListMap { + sl.insertAfter(f, curNode) - // 跳表的节点定义,key是可以比较的 - public static class SkipListNode, V> { - public K key; - public V val; - // 0 nextNodes.get(0) - // 1 nextNodes.get(1) - // i nextNodes.get(i) - // nextNodes.size() - // 多条指向其他节点的指针 - public ArrayList> nextNodes; + rander := rand.New(rand.NewSource(time.Now().UnixNano())) - public SkipListNode(K k, V v) { - key = k; - val = v; - // node 7层指针 - // nextNodes.add(null) - // nextNodes.add(null) - // nextNodes.add(null) - // nextNodes.add(null) - // nextNodes.add(null) - // nextNodes.add(null) - // nextNodes.add(null) - nextNodes = new ArrayList>(); + curlevels := 1 + for rander.Intn(UP_LEVELS_TOTAL) < UP_LEVELS_ABILITY { + curlevels++ + if curlevels > sl.levels { + sl.newlevels() } - // 遍历的时候,如果是往右遍历到的null(next == null), 遍历结束 - // 头(null), 头节点的null,认为最小 - // node -> 头,node(null, "") node.isKeyLess(!null) true - // node里面的key是否比otherKey小,true,不是false - // 当前node里面的key是否比传进来的key要小。小返回true,不小返回false - public boolean isKeyLess(K otherKey) { - // otherKey == null -> false - return otherKey != null && (key == null || key.compareTo(otherKey) < 0); + for f.up == nil { + f = f.pre } + f = f.up + tmpNode := &skipListNode{score: score} - // 判断当前node的key可传入的key是否相等 - public boolean isKeyEqual(K otherKey) { - return (key == null && otherKey == null) - || (key != null && otherKey != null && key.compareTo(otherKey) == 0); - } + curNode.up = tmpNode + tmpNode.down = curNode + sl.insertAfter(f, tmpNode) + curNode = tmpNode } - public static class SkipListMap, V> { - // 随机建层的随机数 - private static final double PROBABILITY = 0.5; // < 0.5 继续做,>=0.5 停 - private SkipListNode head; - private int size; - private int maxLevel; - - public SkipListMap() { - head = new SkipListNode(null, null); - head.nextNodes.add(null); - size = 0; - maxLevel = 0; - } + sl.size++ +} - // 从最高层开始,一路找下去, - // 最终,找到第0层的 mostRightLessNodeInTree(K key) { - if (key == null) { - return null; - } - int level = maxLevel; - SkipListNode cur = head; - while (level >= 0) { // 从上层跳下层,直到跳到0层 - // cur level -> level-1 - cur = mostRightLessNodeInLevel(key, cur, level--); - } - return cur; - } +func (sl *skipList) Remove(score int64) interface{} { + f := sl.findNode(score) + if f.score != score { + return nil + } + v := f.val - // 在level层里,如何往右移动 - // 现在来到的节点是cur,来到了cur的level层,在level层上,找到 mostRightLessNodeInLevel(K key, - SkipListNode cur, - int level) { - SkipListNode next = cur.nextNodes.get(level); - while (next != null && next.isKeyLess(key)) { - cur = next; - next = cur.nextNodes.get(level); - } - return cur; - } + for f != nil { + f.pre.next = f.next + f.next.pre = f.pre + f = f.up + } + return v +} - public boolean containsKey(K key) { - if (key == null) { - return false; - } - SkipListNode less = mostRightLessNodeInTree(key); - SkipListNode next = less.nextNodes.get(0); - return next != null && next.isKeyEqual(key); - } +func (sl *skipList) newlevels() { + nhead := &skipListNode{score: math.MinInt64} + ntail := &skipListNode{score: math.MaxInt64} + nhead.next = ntail + ntail.pre = nhead - // put进来一个节点 - public void put(K key, V value) { - if (key == null) { - return; - } - // 找到小于key的最右的节点。从上层往下层跳的方式找最底层对应的节点,最底层数据是全的,直接遍历最底层就编程O(N)复杂度了 - SkipListNode less = mostRightLessNodeInTree(key); - SkipListNode find = less.nextNodes.get(0); - // 找到的不是null,那么就是重复key,更新key对应的值 - if (find != null && find.isKeyEqual(key)) { - find.val = value; - } else { // 否则没找到,新增一个key,size++ - size++; - int newNodeLevel = 0; - while (Math.random() < PROBABILITY) { - newNodeLevel++; - } - // 如果当前节点的随机引用个数,大于起始节点的指针个数,起始节点的指针个数要更新为大的这个 - while (newNodeLevel > maxLevel) { - head.nextNodes.add(null); - maxLevel++; - } - // 建立新节点 - SkipListNode newNode = new SkipListNode(key, value); - for (int i = 0; i <= newNodeLevel; i++) { - newNode.nextNodes.add(null); - } - int level = maxLevel; - SkipListNode pre = head; - while (level >= 0) { - // 在level层找到最右的小于key的节点,赋值给pre - pre = mostRightLessNodeInLevel(key, pre, level); - // 找到小于等于当前节点的随机层数小于的层数时,该节点的影响层数都要更新到其他层 - if (level <= newNodeLevel) { - newNode.nextNodes.set(level, pre.nextNodes.get(level)); - pre.nextNodes.set(level, newNode); - } - level--; - } - } - } + sl.head.up = nhead + nhead.down = sl.head + sl.tail.up = ntail + ntail.down = sl.tail - public V get(K key) { - if (key == null) { - return null; - } - SkipListNode less = mostRightLessNodeInTree(key); - SkipListNode next = less.nextNodes.get(0); - return next != null && next.isKeyEqual(key) ? next.val : null; - } + sl.head = nhead + sl.tail = ntail + sl.levels++ +} - public void remove(K key) { - if (containsKey(key)) { - size--; - int level = maxLevel; - SkipListNode pre = head; - while (level >= 0) { - pre = mostRightLessNodeInLevel(key, pre, level); - SkipListNode next = pre.nextNodes.get(level); - // 1)在这一层中,pre下一个就是key - // 2)在这一层中,pre的下一个key是>要删除key - if (next != null && next.isKeyEqual(key)) { - // free delete node memory -> C++ - // level : pre -> next(key) -> ... - pre.nextNodes.set(level, next.nextNodes.get(level)); - } - // 在level层只有一个节点了,就是默认节点head - if (level != 0 && pre == head && pre.nextNodes.get(level) == null) { - head.nextNodes.remove(level); - maxLevel--; - } - level--; - } - } - } +func (sl *skipList) insertAfter(pNode *skipListNode, curNode *skipListNode) { + curNode.next = pNode.next + curNode.pre = pNode + pNode.next.pre = curNode + pNode.next = curNode +} - public K firstKey() { - return head.nextNodes.get(0) != null ? head.nextNodes.get(0).key : null; - } +func (sl *skipList) findNode(score int64) *skipListNode { + p := sl.head - public K lastKey() { - int level = maxLevel; - SkipListNode cur = head; - while (level >= 0) { - SkipListNode next = cur.nextNodes.get(level); - while (next != null) { - cur = next; - next = cur.nextNodes.get(level); - } - level--; + for p != nil { + if p.score == score { + if p.down == nil { + return p } - return cur.key; - } - - public K ceillingKey(K key) { - if (key == null) { - return null; + p = p.down + } else if p.score < score { + if p.next.score > score { + if p.down == nil { + return p + } + p = p.down + } else { + p = p.next } - SkipListNode less = mostRightLessNodeInTree(key); - SkipListNode next = less.nextNodes.get(0); - return next != null ? next.key : null; } + } + return p +} - public K floorKey(K key) { - if (key == null) { - return null; - } - SkipListNode less = mostRightLessNodeInTree(key); - SkipListNode next = less.nextNodes.get(0); - return next != null && next.isKeyEqual(key) ? next.key : less.key; - } +func (sl *skipList) Print() { - public int size() { - return size; - } + mapScore := make(map[int64]int) + p := sl.head + for p.down != nil { + p = p.down } - - // for test - public static void printAll(SkipListMap obj) { - for (int i = obj.maxLevel; i >= 0; i--) { - System.out.print("Level " + i + " : "); - SkipListNode cur = obj.head; - while (cur.nextNodes.get(i) != null) { - SkipListNode next = cur.nextNodes.get(i); - System.out.print("(" + next.key + " , " + next.val + ") "); - cur = next; + index := 0 + for p != nil { + mapScore[p.score] = index + p = p.next + index++ + } + p = sl.head + for i := 0; i < sl.levels; i++ { + q := p + preIndex := 0 + for q != nil { + s := q.score + if s == math.MinInt64 { + fmt.Printf("%s", "BEGIN") + q = q.next + continue + } + index := mapScore[s] + c := (index - preIndex - 1) * 6 + for m := 0; m < c; m++ { + fmt.Print("-") + } + if s == math.MaxInt64 { + fmt.Printf("-->%s\n", "END") + } else { + fmt.Printf("-->%3d", s) + preIndex = index } - System.out.println(); + q = q.next } + p = p.down } - - public static void main(String[] args) { - SkipListMap test = new SkipListMap<>(); - printAll(test); - System.out.println("======================"); - test.put("A", "10"); - printAll(test); - System.out.println("======================"); - test.remove("A"); - printAll(test); - System.out.println("======================"); - test.put("E", "E"); - test.put("B", "B"); - test.put("A", "A"); - test.put("F", "F"); - test.put("C", "C"); - test.put("D", "D"); - printAll(test); - System.out.println("======================"); - System.out.println(test.containsKey("B")); - System.out.println(test.containsKey("Z")); - System.out.println(test.firstKey()); - System.out.println(test.lastKey()); - System.out.println(test.floorKey("D")); - System.out.println(test.ceillingKey("D")); - System.out.println("======================"); - test.remove("D"); - printAll(test); - System.out.println("======================"); - System.out.println(test.floorKey("D")); - System.out.println(test.ceillingKey("D")); - - } - } +func main() { + sk := NewSkipList() + + sk.Insert(100, "lala") + sk.Insert(11, "sx") + sk.Insert(22, "11") + sk.Insert(3, "dd") + sk.Insert(80, "bb") + sk.Insert(77, "bb") + sk.Insert(6, "bb") + sk.Insert(88, "bb") + sk.Insert(33, "bb") + sk.Insert(44, "bb") + + //fmt.Println(sk.Get(22)) + //fmt.Println(sk.Get(55)) + //fmt.Println(sk.Remove(22)) + //fmt.Println(sk.Get(22)) + //fmt.Println(sk.Size()) + //fmt.Println(sk.Layout()) + sk.Print() +} ``` @@ -2066,8 +1416,6 @@ public class Code03_SkipListMap { 最后全局最小区间,就是我们要找的区间; - - > 解题思路:实质上解题流程,是在尝试每一个数字开头的情况下,哪个区间是最小的。以每一个数字去尝试,实质上是一种贪心思想,不去考虑数字不以数组中出现的区间,该区间一定不是最小的 @@ -2094,256 +1442,4 @@ public class Code03_SkipListMap { > 要实现这样的功能,系统实现的有序表,无法实现,一方面原始有序表无法加入重复数字,第二方面没有这样的方法返回个数。这样的方法,可以实现为,小于a的数有多少个,小于b的数有多少个,那么最终我们需要的个数就是a-b个 - - -在SB树上改造: - -```Java -package class07; - -import java.util.HashSet; - -public class Code01_CountofRangeSum { - - public static int countRangeSum1(int[] nums, int lower, int upper) { - int n = nums.length; - long[] sums = new long[n + 1]; - for (int i = 0; i < n; ++i) - sums[i + 1] = sums[i] + nums[i]; - return countWhileMergeSort(sums, 0, n + 1, lower, upper); - } - - // leetcode不太好理解的版本 - private static int countWhileMergeSort(long[] sums, int start, int end, int lower, int upper) { - if (end - start <= 1) - return 0; - int mid = (start + end) / 2; - int count = countWhileMergeSort(sums, start, mid, lower, upper) - + countWhileMergeSort(sums, mid, end, lower, upper); - int j = mid, k = mid, t = mid; - long[] cache = new long[end - start]; - for (int i = start, r = 0; i < mid; ++i, ++r) { - while (k < end && sums[k] - sums[i] < lower) - k++; - while (j < end && sums[j] - sums[i] <= upper) - j++; - while (t < end && sums[t] < sums[i]) - cache[r++] = sums[t++]; - cache[r] = sums[i]; - count += j - k; - } - System.arraycopy(cache, 0, sums, start, t - start); - return count; - } - - // 节点改造为,有自己的key,有左右孩子,有size,有词频数量all。all要和size同样维护起来 - public static class SBTNode { - public long key; - public SBTNode l; - public SBTNode r; - public long size; // 不同key的size,sb树的平衡指标 - public long all; // 总的size - - public SBTNode(long k) { - key = k; - size = 1; - all = 1; - } - } - - public static class SizeBalancedTreeSet { - private SBTNode root; - private HashSet set = new HashSet<>(); - - private SBTNode rightRotate(SBTNode cur) { - long same = cur.all - (cur.l != null ? cur.l.all : 0) - (cur.r != null ? cur.r.all : 0); - SBTNode leftNode = cur.l; - cur.l = leftNode.r; - leftNode.r = cur; - leftNode.size = cur.size; - cur.size = (cur.l != null ? cur.l.size : 0) + (cur.r != null ? cur.r.size : 0) + 1; - // all modify - leftNode.all = cur.all; - cur.all = (cur.l != null ? cur.l.all : 0) + (cur.r != null ? cur.r.all : 0) + same; - return leftNode; - } - - private SBTNode leftRotate(SBTNode cur) { - long same = cur.all - (cur.l != null ? cur.l.all : 0) - (cur.r != null ? cur.r.all : 0); - SBTNode rightNode = cur.r; - cur.r = rightNode.l; - rightNode.l = cur; - rightNode.size = cur.size; - cur.size = (cur.l != null ? cur.l.size : 0) + (cur.r != null ? cur.r.size : 0) + 1; - // all modify - rightNode.all = cur.all; - cur.all = (cur.l != null ? cur.l.all : 0) + (cur.r != null ? cur.r.all : 0) + same; - return rightNode; - } - - private SBTNode matain(SBTNode cur) { - if (cur == null) { - return null; - } - if (cur.l != null && cur.l.l != null && cur.r != null && cur.l.l.size > cur.r.size) { - cur = rightRotate(cur); - cur.r = matain(cur.r); - cur = matain(cur); - } else if (cur.l != null && cur.l.r != null && cur.r != null && cur.l.r.size > cur.r.size) { - cur.l = leftRotate(cur.l); - cur = rightRotate(cur); - cur.l = matain(cur.l); - cur.r = matain(cur.r); - cur = matain(cur); - } else if (cur.r != null && cur.r.r != null && cur.l != null && cur.r.r.size > cur.l.size) { - cur = leftRotate(cur); - cur.l = matain(cur.l); - cur = matain(cur); - } else if (cur.r != null && cur.r.l != null && cur.l != null && cur.r.l.size > cur.l.size) { - cur.r = rightRotate(cur.r); - cur = leftRotate(cur); - cur.l = matain(cur.l); - cur.r = matain(cur.r); - cur = matain(cur); - } - return cur; - } - - // add方法时,all要跟着增加 - private SBTNode add(SBTNode cur, long key, boolean contains) { - if (cur == null) { - return new SBTNode(key); - } else { - cur.all++; - if (key == cur.key) { - return cur; - } else { // 还在左滑或者右滑 - if (!contains) { - cur.size++; - } - if (key < cur.key) { - cur.l = add(cur.l, key, contains); - } else { - cur.r = add(cur.r, key, contains); - } - return matain(cur); - } - } - } - - public void add(long sum) { - boolean contains = set.contains(sum); - root = add(root, sum, contains); - set.add(sum); - } - - // 本题在原始有序表上增加的方法,小于一个key的个数有多少个 - public long lessKeySize(long key) { - SBTNode cur = root; - long ans = 0; - while (cur != null) { - if (key == cur.key) { - return ans + (cur.l != null ? cur.l.all : 0); - } else if (key < cur.key) { - cur = cur.l; - } else { - ans += cur.all - (cur.r != null ? cur.r.all : 0); - cur = cur.r; - } - } - return ans; - } - - // > 7 8... - // <8 ...<=7 - public long moreKeySize(long key) { - return root != null ? (root.all - lessKeySize(key + 1)) : 0; - } - - } - - // 好理解的版本,求[a,b]上满足数量的个数 - public static int countRangeSum2(int[] nums, int lower, int upper) { - SizeBalancedTreeSet treeSet = new SizeBalancedTreeSet(); - long sum = 0; - int ans = 0; - // 一个数都没有的时候词频是0 - treeSet.add(0); - for (int i = 0; i < nums.length; i++) { - sum += nums[i]; - // sum = x [a,b] start > x-a -start < -x+a x-start < a - // [a,b] - // < a > b - // i + 1 -x+b x-start > b - long moreUppers = treeSet.lessKeySize(sum - upper); - ans += i + 1 - lessLowers - moreUppers; - treeSet.add(sum); - } - return ans; - } - - // for test - public static void printArray(int[] arr) { - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - // for test - public static int[] generateArray(int len, int varible) { - int[] arr = new int[len]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) (Math.random() * varible); - } - return arr; - } - - public static void main(String[] args) { - int len = 200; - int varible = 50; - for (int i = 0; i < 10000; i++) { - int[] test = generateArray(len, varible); - int lower = (int) (Math.random() * varible) - (int) (Math.random() * varible); - int upper = lower + (int) (Math.random() * varible); - int ans1 = countRangeSum1(test, lower, upper); - int ans2 = countRangeSum2(test, lower, upper); - if (ans1 != ans2) { - printArray(test); - System.out.println(lower); - System.out.println(upper); - System.out.println(ans1); - System.out.println(ans2); - } - } - - } - -} - -``` - - -> 有序表结构本身比较重要,我们也经常使用系统实现的有序表,但是涉及到手动改有序表的实现,本身就已经比较难,而且面试出现的概率不是很高 - - -Java的TreeMap底层是红黑树,但是SB树完全可以替换,没任何差别 - - - - - - - - - - - - - - - - - +> 有序表结构本身比较重要,我们也经常使用系统实现的有序表,但是涉及到手动改有序表的实现,本身就已经比较难,而且面试出现的概率不是很高 \ No newline at end of file From 928104ad30a99f3743b3a5df41c3a2843b3f0c34 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Mon, 4 Apr 2022 16:45:15 +0800 Subject: [PATCH 09/18] ac --- ...41\347\211\271\345\205\260\346\225\260.md" | 248 +++++++++--------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git "a/24-\343\200\212\350\277\233\351\230\266\343\200\213AC\350\207\252\345\212\250\346\234\272\345\222\214\345\215\241\347\211\271\345\205\260\346\225\260.md" "b/24-\343\200\212\350\277\233\351\230\266\343\200\213AC\350\207\252\345\212\250\346\234\272\345\222\214\345\215\241\347\211\271\345\205\260\346\225\260.md" index ee7c6b7..42e0fde 100644 --- "a/24-\343\200\212\350\277\233\351\230\266\343\200\213AC\350\207\252\345\212\250\346\234\272\345\222\214\345\215\241\347\211\271\345\205\260\346\225\260.md" +++ "b/24-\343\200\212\350\277\233\351\230\266\343\200\213AC\350\207\252\345\212\250\346\234\272\345\222\214\345\215\241\347\211\271\345\205\260\346\225\260.md" @@ -10,146 +10,146 @@ AC自动机要解决的问题是,在一个文章中,有一些候选字符串 为每一个候选串建立一个前缀树,每个树节点都有一个fail指针。头节点fail指针人为规定指向null,第一层节点的fail指针人为规定,指向头节点。建立好前缀树后,宽度优先遍历设置全部的fail指针 -> 比较绕,看不懂看代码 - -宽度优先遍历设置fali的指针的过程,如果某个节点的指针指向null,孩子的fail指针指向当前的父亲;如果某个节点的fail指针指向不为空的节点A,A孩子的路径为B,那么A的fali指针有没有指向B的路径,如果有,A孩子的fail指针,指向父亲节点的fail指针指向的B;如果父亲没有指向B的路,再找fail直到为null后,孩子fail指针指向头结点 - - - -```Java -package class08; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; - -public class Code01_AC { - - // 前缀树的节点 - public static class Node { - // 如果一个node,end为空,不是结尾 - // 如果end不为空,表示这个点是某个字符串的结尾,end的值就是这个字符串 - public String end; - // 只有在上面的end变量不为空的时候,endUse才有意义 - // 表示,这个字符串之前有没有加入过答案 - public boolean endUse; - public Node fail; - public Node[] nexts; - public Node() { - endUse = false; - end = null; - fail = null; - // 假设前缀树的节点上的值只是小写字母,有26个指向。经典前缀树 - nexts = new Node[26]; - } +> 比较绕,可以考虑看代码详细步骤来理解 + +宽度优先遍历设置fail的指针的过程,如果某个节点的指针指向null,孩子的fail指针指向当前的父亲;如果某个节点的fail指针指向不为空的节点A,A孩子的路径为B,那么A的fail指针有没有指向B的路径,如果有,A孩子的fail指针,指向父亲节点的fail指针指向的B;如果父亲没有指向B的路,再找fail直到为null后,孩子fail指针指向头结点 + + + +```Go +package main + +import "fmt" + +// Node 前缀树的节点 +type Node struct { + // 如果一个node,end为空,不是结尾 + // 如果end不为空,表示这个点是某个字符串的结尾,end的值就是这个字符串 + End string + // 只有在上面的end变量不为空的时候,endUse才有意义 + // 表示,这个字符串之前有没有加入过答案 + EndUse bool + Fail *Node + // 假设前缀树的节点上的值只是小写字母,有26个指向。经典前缀树 + Nexts []*Node +} + +func InitACAutomationNode() *Node { + root := &Node{ + End: "", + EndUse: false, + Fail: new(Node), + Nexts: make([]*Node, 26), } - - // AC自动机 - public static class ACAutomation { - private Node root; - - // 建头结点 - public ACAutomation() { - root = new Node(); - } + return root +} - // 先建前缀树,建好之后再build所有节点的fail指针 - public void insert(String s) { - char[] str = s.toCharArray(); - Node cur = root; - int index = 0; - for (int i = 0; i < str.length; i++) { - index = str[i] - 'a'; - if (cur.nexts[index] == null) { - Node next = new Node(); - cur.nexts[index] = next; - } - cur = cur.nexts[index]; - } - cur.end = s; +// insert 先建前缀树,建好之后再build所有节点的fail指针 +func (root *Node) insert(s string) { + str := []byte(s) + cur := root + index := 0 + for i := 0; i < len(str); i++ { + index = int(str[i] - 'a') + if cur.Nexts[index] == nil { + next := InitACAutomationNode() + cur.Nexts[index] = next } + cur = cur.Nexts[index] + } + cur.End = s +} - // 建立所有节点的fail指针 - public void build() { - Queue queue = new LinkedList<>(); - queue.add(root); - Node cur = null; - Node cfail = null; - while (!queue.isEmpty()) { - // 当前节点弹出, - // 当前节点的所有后代加入到队列里去, - // 当前节点给它的子去设置fail指针 - // cur -> 父亲 - cur = queue.poll(); - for (int i = 0; i < 26; i++) { // 所有的路 - if (cur.nexts[i] != null) { // 找到所有有效的路 - cur.nexts[i].fail = root; // - cfail = cur.fail; - while (cfail != null) { - if (cfail.nexts[i] != null) { - cur.nexts[i].fail = cfail.nexts[i]; - break; - } - cfail = cfail.fail; - } - queue.add(cur.nexts[i]); +// 建立所有节点的fail指针 +func (root *Node) build() { + queue := make([]*Node, 0) + queue = append(queue, root) + var cur *Node + var cfail *Node + + for len(queue) != 0 { + // 当前节点弹出, + // 当前节点的所有后代加入到队列里去, + // 当前节点给它的子去设置fail指针 + // cur -> 父亲 + cur = queue[0] + queue = queue[1:] + + for i := 0; i < 26; i++ { // 所有的路 + if cur != nil && cur.Nexts != nil && cur.Nexts[i] != nil { // 找到所有有效的路 + cur.Nexts[i].Fail = root + cfail = cur.Fail + + for cfail != nil { + if cfail.Nexts != nil && cfail.Nexts[i] != nil { + cur.Nexts[i].Fail = cfail.Nexts[i] + break } + cfail = cfail.Fail } + queue = append(queue, cur.Nexts[i]) } } + } +} - // build好之后,可以查文章有哪些候选串 - public List containWords(String content) { - char[] str = content.toCharArray(); - Node cur = root; - Node follow = null; - int index = 0; - List ans = new ArrayList<>(); - for (int i = 0; i < str.length; i++) { - index = str[i] - 'a'; // 路 - // 如果当前字符在这条路上没配出来,就随着fail方向走向下条路径 - while (cur.nexts[index] == null && cur != root) { - cur = cur.fail; - } - // 1) 现在来到的路径,是可以继续匹配的 - // 2) 现在来到的节点,就是前缀树的根节点 - cur = cur.nexts[index] != null ? cur.nexts[index] : root; - follow = cur; - while (follow != root) { - if(follow.endUse) { - break; - } - // 不同的需求,在这一段之间修改 - if (follow.end != null) { - ans.add(follow.end); - follow.endUse = true; - } - // 不同的需求,在这一段之间修改 - follow = follow.fail; - } - } - return ans; + +// build好之后,可以查文章有哪些候选串 +func (root *Node) containWords(content string) []string { + str := []byte(content) + + cur := root + var follow *Node + ans := make([]string, 0) + + for i := 0; i < len(str); i++ { + index := int(str[i] - 'a') // 路 + // 如果当前字符在这条路上没配出来,就随着fail方向走向下条路径 + for cur.Nexts[index] == nil && cur != root { + cur = cur.Fail } - } + // 1) 现在来到的路径,是可以继续匹配的 + // 2) 现在来到的节点,就是前缀树的根节点 + if cur.Nexts[index] != nil { + cur = cur.Nexts[index] + } else { + cur = root + } + follow = cur + + for follow != root { + if follow.EndUse { + break + } - public static void main(String[] args) { - ACAutomation ac = new ACAutomation(); - ac.insert("dhe"); - ac.insert("he"); - ac.insert("abcdheks"); - // 设置fail指针 - ac.build(); - - List contains = ac.containWords("abcdhekskdjfafhasldkflskdjhwqaeruv"); - for (String word : contains) { - System.out.println(word); + // 不同的需求,在这一段之间修改 + if len(follow.End) != 0 { + ans = append(ans, follow.End) + follow.EndUse = true + } + // 不同的需求,在这一段之间修改 + follow = follow.Fail } } - + return ans } +//he +//abcdheks +func main() { + ac := InitACAutomationNode() + ac.insert("ahe") + ac.insert("he") + ac.insert("abcdheks") + // 设置fail指针 + ac.build() + + contains := ac.containWords("abcdhekskdjfafhasldkflskdjhwqaeruv") + for _, word := range contains { + fmt.Println(word) + } +} ``` From ae2c90388a2989d6bca9e25cd7856300691bd8f0 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Mon, 4 Apr 2022 16:45:33 +0800 Subject: [PATCH 10/18] limit q --- ...\231\220\345\210\266\347\261\273\351\227\256\351\242\230.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/22-\343\200\212\350\277\233\351\230\266\343\200\213\350\265\204\346\272\220\351\231\220\345\210\266\347\261\273\351\227\256\351\242\230.md" "b/22-\343\200\212\350\277\233\351\230\266\343\200\213\350\265\204\346\272\220\351\231\220\345\210\266\347\261\273\351\227\256\351\242\230.md" index 283751e..4f6dc66 100644 --- "a/22-\343\200\212\350\277\233\351\230\266\343\200\213\350\265\204\346\272\220\351\231\220\345\210\266\347\261\273\351\227\256\351\242\230.md" +++ "b/22-\343\200\212\350\277\233\351\230\266\343\200\213\350\265\204\346\272\220\351\231\220\345\210\266\347\261\273\351\227\256\351\242\230.md" @@ -36,7 +36,7 @@ - 第一问题解 利用位图 -如果限制1GB,那么可以使用位图,0到2的32次方减1范围的无符号数,只需要2的32次方个bit来存记录。Hash表需要4个字节才能表示一个数出现过还是没出现过,Bit来代表一个数出现过还是没出现过,空间上缩小了32倍。原本使用Hash需要的16G空间,现在缩小32倍,大约500M可以拿下==对应第五点== +如果限制1GB,那么可以使用位图,0到2的32次方减1范围的无符号数,只需要2的32次方个bit来存记录。Hash表需要4个字节才能表示一个数出现过还是没出现过,Bit来代表一个数出现过还是没出现过,空间上缩小了32倍。原本使用Hash需要的16G空间,现在缩小32倍,大约500M可以拿下,对应上述第五点 - 第二问和第三问题解 利用分段统计 From f1accd73532f604d670073e4d41a9cd45ecc0c2a Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Mon, 4 Apr 2022 16:45:53 +0800 Subject: [PATCH 11/18] hash and bit table ... --- ...03\351\232\206\345\217\212\345\262\233.md" | 390 ++++++++++-------- 1 file changed, 227 insertions(+), 163 deletions(-) rename "21-\343\200\212\350\277\233\351\230\266\343\200\213\345\223\210\345\270\214\345\207\275\346\225\260\347\273\223\346\236\204\345\217\212\345\262\233\351\227\256\351\242\230.md" => "21-\343\200\212\350\277\233\351\230\266\343\200\213\345\223\210\345\270\214\343\200\201\344\275\215\345\233\276\343\200\201\345\270\203\351\232\206\345\217\212\345\262\233.md" (67%) diff --git "a/21-\343\200\212\350\277\233\351\230\266\343\200\213\345\223\210\345\270\214\345\207\275\346\225\260\347\273\223\346\236\204\345\217\212\345\262\233\351\227\256\351\242\230.md" "b/21-\343\200\212\350\277\233\351\230\266\343\200\213\345\223\210\345\270\214\343\200\201\344\275\215\345\233\276\343\200\201\345\270\203\351\232\206\345\217\212\345\262\233.md" similarity index 67% rename from "21-\343\200\212\350\277\233\351\230\266\343\200\213\345\223\210\345\270\214\345\207\275\346\225\260\347\273\223\346\236\204\345\217\212\345\262\233\351\227\256\351\242\230.md" rename to "21-\343\200\212\350\277\233\351\230\266\343\200\213\345\223\210\345\270\214\343\200\201\344\275\215\345\233\276\343\200\201\345\270\203\351\232\206\345\217\212\345\262\233.md" index a5a7bf0..d92394b 100644 --- "a/21-\343\200\212\350\277\233\351\230\266\343\200\213\345\223\210\345\270\214\345\207\275\346\225\260\347\273\223\346\236\204\345\217\212\345\262\233\351\227\256\351\242\230.md" +++ "b/21-\343\200\212\350\277\233\351\230\266\343\200\213\345\223\210\345\270\214\343\200\201\344\275\215\345\233\276\343\200\201\345\270\203\351\232\206\345\217\212\345\262\233.md" @@ -19,7 +19,6 @@ 通过hash散列之后的值,如果模上一个数,模之后的数仍然是散列的!具有传递性 - > 对于两个输入,即使长的再像,hash函数也会散列,均分分布在值域上 @@ -57,15 +56,19 @@ 假设我们get我们的'abc',通过相同的函数算出hash值,再模17,肯定得到5,我们去5的下面去遍历链表,找到'abc'拿出它的值即可 -由于使用的hash值进行填充,理论上我们17长度的数组,很容易就碰撞,而且每个格子均匀的碰撞。我们感知到某个位置上的链表长度大于等于6,可以认为其他格子的链表长度也差不多大于等于6。此时我们进行hash扩容,增大我们的数组长度。假设扩容为原数组的两倍,范围为0到34;==**接着我们用非常暴力的手段,把老的数组上的每个链表上的节点都取出来,重新计算hash值,模34,决定放在新数组的哪个位置,那么根据散列的性质,我们大概知道,样本分配到新的数组中,每个位置链表的长度,大概为3**== +由于使用的hash值进行填充,理论上我们17长度的数组,很容易就碰撞,而且每个格子均匀的碰撞。我们感知到某个位置上的链表长度大于等于6,可以认为其他格子的链表长度也差不多大于等于6。此时我们进行hash扩容,增大我们的数组长度。假设扩容为原数组的两倍,范围为0到34; + +**紧接着我们用非常暴力的手段,把老的数组上的每个链表上的节点都取出来,重新计算hash值,模34,决定放在新数组的哪个位置,那么根据散列的性质,我们大概知道,样本分配到新的数组中,每个位置链表的长度,大概为3** -复杂度分析:对于一个key_value,算key的hash值认为O(1);hash值模一个值O(1);找到数组中的桶,而且桶长度不超过6,那么也是O(1)。所以不涉及到扩容,hash结增删改查操作严格O(1);**但是Hash函数是会涉及到扩容的,我们可以假设初始结构的数组长度为2进行推演,那么对于样本容量为N的hash结构,如果扩容,那么N长度的哈希结构会经历log2N,log以2为底的N次。每次扩容的代价时间复杂度为O(N), 对于N样本,之前所有扩容的总代价为O(N)✖log2N。均摊到每次hash值的增删改查,所有hash表考虑扩容的增删改查时间复杂度不是O(1)。而是(O(N)✖log2N)➗* N* +复杂度分析:对于一个key_value,算key的hash值认为O(1);hash值模一个值O(1);找到数组中的桶,而且桶长度不超过6,那么也是O(1)。所以不涉及到扩容,hash结增删改查操作严格O(1); +**但是Hash函数是会涉及到扩容的,我们可以假设初始结构的数组长度为2进行推演,那么对于样本容量为N的hash结构,如果扩容,那么N长度的哈希结构会经历log2N,log以2为底的N次。每次扩容的代价时间复杂度为O(N), 对于N样本,之前所有扩容的总代价为O(N)✖log2N。均摊到每次hash值的增删改查,所有hash表考虑扩容的增删改查时间复杂度不是O(1)。而是`(O(N)✖log2N)➗ N`** -> 但是Hash表有很多改进,比如扩容倍数不是扩两倍,而是扩更多被,用以减少扩容次数,从而减少log的底数。或者假设在Jvm中,用户申请的hash表不够用了,JVM离线扩容,用户无感知;或者我们在数组中放有序表而不是单链表,例如treeSet结构,我们长度就不需要到6再扩容了,等等。我们还是认为hash表在使用时,增删改查操作就是O(1)的,虽然理论上并不是 -> !!!上述订正,注意上述说扩容代价为O(logN)可以认为为O(1),这种说法是错误的,hash扩容总代价是O(N),均摊下来就是O(1)。因为当我们数据量已经扩容到N,之前的扩容是一次一次叠加而来,可以假设从样本为1的时候开始扩容,N的数据量对应的扩容就是1+2+...+n/4+n/2; 均摊到当前的N,就是O(1) +> 但是Hash表有很多改进,比如扩容倍数不是扩两倍,而是扩更多倍,用以减少扩容次数,从而减少log的底数。或者假设在Jvm中,用户申请的hash表不够用了,JVM离线扩容,用户无感知;或者我们在数组中放有序表而不是单链表,例如treeSet结构,我们长度就不需要到6再扩容了,等等。我们还是认为hash表在使用时,增删改查操作就是O(1)的,虽然理论上并不是 + +> 注意!!!上述估计hash扩容代价的订正,注意上述说扩容代价为O(logN)可以认为为O(1),这种说法是错误的,hash扩容总代价是O(N),均摊下来就是O(1)。因为当我们数据量已经扩容到N,之前的扩容是一次一次叠加而来,可以假设从样本为1的时候开始扩容,N的数据量对应的扩容就是1+2+...+n/4+n/2; 均摊到当前的N,就是O(1) # 2 布隆过滤器 @@ -87,7 +90,7 @@ 假设我们申请int[100] arr的数组大小,每一个位置是int32位,100个位置总共需要3200bit; -假设我们想要知道arr的位图的432位是什么数字,那么我们可以立马知道423位bit的状态是0还是1,`int status = (arr[453/32] >> (453%32)) & 1;`;我们也可以设置453位的状态,比如我们要设置453位的状态为1:`arr[453/32] = arr[453/32] | (1 << (453%32));`; +假设我们想要知道arr的位图的453位是什么数字,那么我们可以立马知道453位bit的状态是0还是1,`int status = (arr[453/32] >> (453%32)) & 1;`;我们也可以设置453位的状态,比如我们要设置453位的状态为1:`arr[453/32] = arr[453/32] | (1 << (453%32));`; 对于数据量比较大的存储,我们可以使用二维数组,比如int[100][100] 表示的总共有320000个bit。我们想要把170039位置的比特拿出来,可以先求在哪一行,`170039/3200`等于53行,在53行的`(170039 % 3200 )/32 `位置,在该位置的`(170039 % 3200 )%32`bit的位置上 @@ -96,7 +99,7 @@ 布隆过滤器建立在位图的概念上。 -### 2.2.1 布隆过滤去的添加 +### 2.2.1 布隆过滤器的添加 假设我们有m长度的位图,初始化位图都为0,某一个位置需要置为1,就属于描黑的状态。我们给每一个url算出一个hash值,让该hash值模上m,决定一个位置进行描黑,再用另外一个hash函数算出hash值,模上m描黑第二个点,假设需要k个hash函数,那么一个url需要描黑k个位置,k个位置中有可能有重复的描黑点,在不同的hash函数算出相同的hash值的时候会出现这种情况。经过上述的处理,该url属于被加入我们的黑名单 @@ -105,20 +108,17 @@ 给定一个url,相同的我们用k个hash函数算出k个hash值,用这k个hash值模上m,算出k个位置。假设k个位置都属于描黑的状态,就任务该url已经添加进来我们的黑名单系统了 -==比如对于指纹来说,K个hash函数,可以类比为K个提取指纹的方式== +> 比如对于指纹来说,K个hash函数,可以类比为K个提取指纹的方式 布隆过滤器有可能出现误判,例如我们的m数组长度比较小,url比较多,那么所有url都加进来有可能m长度数组的位图全被描黑。那么一个新的url过来,我们误以为已经加入了进来。 -==布隆过滤器虽然有失误率,存在误判。但是不可能判断出一个url本身在黑名单,判断出来不在黑名单,只可能出现一个url不在黑名单,判断出在黑名单这种情况的误判== - -==宁可错杀三千,绝不放过一个,哈哈== +> 布隆过滤器虽然有失误率,存在误判。但是不可能判断出一个url本身在黑名单,判断出来不在黑名单,只可能出现一个url不在黑名单,判断出在黑名单这种情况的误判,即宁可错杀三千,绝不放过一个 所以严格要求不予许有失误率的场景,用不了布隆过滤器 - ### 2.2.3 k个hash函数如何选择,位图m空间选择多大 我们可以根据样本量N,来设计我们的k和m的大小。设计布隆过滤器,我们必须提前知道样本量 @@ -183,13 +183,13 @@ p = (1 - e ^\frac{-nk}{m} )^k 最著名的使用场景是在hdfs分布式文件系统中,有很多个小文件放着各种各样的数据,如何定位一个string在哪个文件里面? -HDFS会把各个小文件维度,每个小文件建立一个布隆过滤器。先看该string在各个小文件里面那些小文件的布隆过滤器中是描黑的状态。反之如果该String在某个布隆过滤器中是描白的,那么该小文件内肯定不存在该String。描黑的小文件中有可能存在该String,接下来把描黑的小文件各个遍历一遍,去找这个String +HDFS会把各个小文件维度,每个小文件建立一个布隆过滤器。先看该string在各个小文件里面哪些小文件的布隆过滤器中是描黑的状态。反之如果该String在某个布隆过滤器中是描白的,那么该小文件内肯定不存在该String。描黑的小文件中有可能存在该String,接下来把描黑的小文件各个遍历一遍,去找这个String > 经典的布隆过滤器是不支持删除的,但是强制支持删除可以对经典布隆过滤器进行改造。比如可以把两个bit当成一个位置,相应的m编程2m; 描黑一个点可以编程01,再描黑10,删除该描黑点,变为01,再删除00。这样改造可以支持删除 -==布隆过滤器的唯一目的,是为了节约空间== +> 布隆过滤器的唯一目的,是为了节约空间 100亿的数据量,预期失误率万分之一以下,30G以内的布隆过滤器可以搞定。且预期失误率为十万分之六 @@ -216,11 +216,9 @@ HDFS会把各个小文件维度,每个小文件建立一个布隆过滤器。 3、现在来说我们的服务都是非常的弹性,一致性hash就是解决上面很突出的问题的。一致性hash既可以保证迁移代价很低,也可以保证新迁移的机器数量负载均衡 - ### 一致性hash的实现思路 -1、在之前存储中,我们算出记录的hash,模上机器树,找到这条记录的归属。现在我们把hash值的结果想象成一个环,比如md5加密的范围是2的64次方减1。我们把0到2的64次方减1的范围想象成一个环 - +1、在之前存储中,我们算出记录的hash,模上机器数,找到这条记录的归属。现在我们把hash值的结果想象成一个环,比如md5加密的范围是2的64次方减1。我们把0到2的64次方减1的范围想象成一个环 2、现在假设我们需要在三台机器上分布式存储数据,我们可以以三台机器的ip不同,或者三台机器的mac地址不同来确定三台机器的归属。比如我们按三台机器的ip @@ -239,16 +237,16 @@ HDFS会把各个小文件维度,每个小文件建立一个布隆过滤器。 出现均分问题,有两个情况,第一个是初始机器如何保证均分环,第二个加机器减机器怎么保证再次均分该环? -==不采用机器的ip去抢环。我们可以准备一张表,分配给m1机器1000个随机字符串,分配给m2也1000个字符串,同样分配1000个随机字符串给m3。然后这3000个字符串算Hash值去均分环,机器和字符串的关系是1000对应1的关系,比如来了某一个数需要存储,我们算出hash值需要分配给某一个字符串抢到环区域。那么我们再根据这个字符串找到背后的实际物理机器,可以类比m1的1000个红点,m2的1000绿点,m3的1000个黄点== +> 不采用机器的ip去抢环。我们可以准备一张表,分配给m1机器1000个随机字符串,分配给m2也1000个字符串,同样分配1000个随机字符串给m3。然后这3000个字符串算Hash值去均分环,机器和字符串的关系是1000对应1的关系,比如来了某一个数需要存储,我们算出hash值需要分配给某一个字符串抢到环区域。那么我们再根据这个字符串找到背后的实际物理机器,可以类比m1的1000个红点,m2的1000绿点,m3的1000个黄点 -==我们新增物理节点,对应着新增加1000个字符串的hash值去抢环,该向哪个字符串夺取环区域和之前Ip的hash加入环的夺取方式相同,同样的删减机器也是一样。这样我们就可以实现负载均衡== +> 我们新增物理节点,对应着新增加1000个字符串的hash值去抢环,该向哪个字符串夺取环区域和之前Ip的hash加入环的夺取方式相同,同样的删减机器也是一样。这样我们就可以实现负载均衡 > 数据量不到400亿,不需要考虑hash碰撞的问题,这3000个字符串算出的hash值是不可能碰撞的。即使碰撞,某一个hash值即是m1的某个字符串落到的位置红点,又是m2的的某个字符串落到的位置绿点,那么数据冗余两份分别存到m1和m2,也不会有大的影响 -实质还是利用hash函数的离散性,可以理解1000个点事某一种味道的香水分子数。三种味道的香水喷到房间里面,闻起来是均匀的; +实质还是利用hash函数的离散性,可以理解1000个点是某一种味道的香水分子数。三种味道的香水喷到房间里面,闻起来是均匀的; ### 利用hash函数的离散性不仅可以实现负载均衡,也可以实现负载管理 @@ -264,9 +262,6 @@ m1机器性能超强,m2和m3分别是m1机器性能的一半,我们可以给 在北京市,一个用户通过手机搜索十公里范围内的所有餐饮店。可以把北京地图按照经纬度切分,比如以天安门为原点,每个商家注册外卖平台的时候,除了要填写老板,店名之类的信息为,还存一条经纬度的记录 - - - # 4 并行算法和岛问题 假设上下为1相连,可以认为是相同的一片岛。斜着连不算 @@ -281,7 +276,7 @@ m1机器性能超强,m2和m3分别是m1机器性能的一半,我们可以给 ``` -单内存,单cpu如何解决,多核心cpu怎么解决 +单内存,单cpu如何解决,多核心cpu怎么解决? > 解题思路1,可以利用感染函数,到了某个位置,如果该位置为1,把自己连同周围相连的1全部感染成2;对于n行m列,时间复杂度为O(N*M)。每个节点遍历一次,递归调用由于改了状态,下面节点的递归往上看,由于为2不再递归,同理其他位置也一样。实质每个位置看了5次 @@ -304,7 +299,7 @@ A和C并,岛数量变为3,集合变为(A B);B和C并,岛数量为2,集 > 有了上述的思想,我们可以把岛切分成任意块,用不同的CPU去跑,然后再用并查集合并 -==并行的思想,在大数据领域很重要== +**并行的思想,在大数据领域很重要** ``` @@ -317,174 +312,243 @@ A和C并,岛数量变为3,集合变为(A B);B和C并,岛数量为2,集 1 1 1 1 1(A点) * (D点) 1 1 1 1 ``` -```Java -package class03; +```Go +package main -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Stack; +import "fmt" -public class Code02_Islands { +func countIslands1(m [][]int) int { + if len(m) == 0 || len(m[0]) == 0 { + return 0 + } - public static int countIslands1(int[][] m) { - if (m == null || m[0] == null) { - return 0; - } - int N = m.length; - int M = m[0].length; - int res = 0; - // 遍历 - for (int i = 0; i < N; i++) { - for (int j = 0; j < M; j++) { - // 如果该位置是1,调用感染函数,岛的数量增加1 - if (m[i][j] == 1) { - res++; - infect(m, i, j, N, M); - } + N := len(m) + M := len(m[0]) + res := 0 + // 遍历 + for i := 0; i < N; i++ { + for j := 0; j < M; j++ { + // 如果该位置是1,调用感染函数,岛的数量增加1 + if m[i][j] == 1 { + res++ + infect(m, i, j, N, M) } } - return res; } + return res +} - // 感染函数 - public static void infect(int[][] m, int i, int j, int N, int M) { - // 行,列越界,或者当前位置不是1,直接返回 - if (i < 0 || i >= N || j < 0 || j >= M || m[i][j] != 1) { - return; - } - // 把当前位置1感染成2 - m[i][j] = 2; - // 检查四周的1,把四周的1也感染 - 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); +// 感染函数 +func infect(m [][]int, i, j, N, M int) { + // 行,列越界,或者当前位置不是1,直接返回 + if i < 0 || i >= N || j < 0 || j >= M || m[i][j] != 1 { + return } - public static class Element { - public V value; + // 把当前位置1感染成2 + m[i][j] = 2 + // 检查四周的1,把四周的1也感染 + 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) +} - public Element(V value) { - this.value = value; - } - } - - public static class UnionFindSet { - // a -> a 生成的点 - public HashMap> elementMap; - public HashMap, Element> fatherMap; - // sizeMap中的key,每一个key都一定是集合的头节点(代表节点) - public HashMap, Integer> sizeMap; - - public UnionFindSet(List list) { - elementMap = new HashMap<>(); - fatherMap = new HashMap<>(); - sizeMap = new HashMap<>(); - for (V value : list) { - Element element = new Element(value); - elementMap.put(value, element); - fatherMap.put(element, element); - sizeMap.put(element, 1); +// 岛问题,并行+并查集解法 +func countIslands2(m [][]int) int { + list := make([]string, 0) + for row := 0; row < len(m); row++ { + for col := 0; col < len(m[0]); col++ { + if m[row][col] == 1 { + position := fmt.Sprintf("%d_%d", row, col) + list = append(list, position) } } - - // 从输入参数element出发,往上一直找,找到不能再往上的头节点,返回 - private Element findHead(Element element) { - // 把往上找的过程中,沿途的点都记录在path里 - Stack> path = new Stack<>(); - while (element != fatherMap.get(element)) { - path.push(element); - element = fatherMap.get(element); - } - while (!path.isEmpty()) { - fatherMap.put(path.pop(), element); + } + unionSet := InitUnionSet(list) + for row := 0; row < len(m); row++ { + for col := 0; col < len(m[0]); col++ { + if m[row][col] == 1 { + // row,col 5, 3 -> 5_3 + position := fmt.Sprintf("%d_%d", row, col) + if row - 1 >= 0 && m[row - 1][col] == 1 { + up := fmt.Sprintf("%d_%d", row - 1, col) + unionSet.Union(up, position) + } + if col - 1 >= 0 && m[row][col - 1] == 1 { + left := fmt.Sprintf("%d_%d", row, col - 1) + unionSet.Union(left, position) + } } - return element; } + } + return unionSet.getSetNum() +} - public boolean isSameSet(V a, V b) { - if (elementMap.containsKey(a) && elementMap.containsKey(b)) { - return findHead(elementMap.get(a)) == findHead(elementMap.get(b)); - } - return false; - } +//3 +//3 +//1 +//1 +func main() { + m1 := [][]int{ + { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 1, 1, 1, 0, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, + { 0, 1, 1, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + } + fmt.Println(countIslands1(m1)) + + m1Other := [][]int{ + { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 1, 1, 1, 0, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, + { 0, 1, 1, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + } + fmt.Println(countIslands2(m1Other)) + + m2 := [][]int{ + { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, + { 0, 1, 1, 0, 0, 0, 1, 1, 0 }, + { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + } + fmt.Println(countIslands1(m2)) + + m2Other := [][]int{ + { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 1, 1, 1, 1, 1, 1, 1, 0 }, + { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, + { 0, 1, 1, 0, 0, 0, 1, 1, 0 }, + { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + } + fmt.Println(countIslands2(m2Other)) +} - public void union(V a, V b) { - if (elementMap.containsKey(a) && elementMap.containsKey(b)) { - Element aF = findHead(elementMap.get(a)); - Element bF = findHead(elementMap.get(b)); - if (aF != bF) { - Element big = sizeMap.get(aF) >= sizeMap.get(bF) ? aF : bF; - Element small = big == aF ? bF : aF; - fatherMap.put(small, big); - sizeMap.put(big, sizeMap.get(aF) + sizeMap.get(bF)); - sizeMap.remove(small); - } - } - } +// UNode 并查集结构中的节点类型 +type UNode struct { + V string +} - public int getSetNum() { - return sizeMap.size(); - } +type UnionSet struct { + // 记录样本到样本代表点的关系。值到代表该值的Node节点的关系映射 + Nodes map[string]*UNode + // 记录某节点到根祖宗节点的关系。 + // 比如b指向a,c指向a,d指向a,a指向自身 + // map中保存的a->a b->a c->a d->a + RootFatherMap map[*UNode]*UNode + // 只有当前点,他是代表点,会在sizeMap中记录该代表点的连通个数 + SizeMap map[*UNode]int +} +// InitUnionSet 初始化一个并查集结构 +func InitUnionSet(values []string) *UnionSet { + us := &UnionSet{} + nodes := make(map[string]*UNode, 0) + fatherMap := make(map[*UNode]*UNode, 0) + sizeMap := make(map[*UNode]int, 0) + for _, v := range values { + node := &UNode{V: v} + nodes[v] = node + fatherMap[node] = node + sizeMap[node] = 1 } - public static int countIslands2(int[][] m) { - List list = new ArrayList<>(); - for (int row = 0; row < m.length; row++) { - for (int col = 0; col < m[0].length; col++) { - if (m[row][col] == 1) { - String position = String.valueOf(row) + "_" + String.valueOf(col); - list.add(position); - } - } - } - UnionFindSet unionSet = new UnionFindSet<>(list); - for (int row = 0; row < m.length; row++) { - for (int col = 0; col < m[0].length; col++) { - if (m[row][col] == 1) { - // row,col 5, 3 -> 5_3 - String position = String.valueOf(row) + "_" + String.valueOf(col); - if (row - 1 >= 0 && m[row - 1][col] == 1) { - String up = String.valueOf(row - 1) + "_" + String.valueOf(col); - unionSet.union(up, position); - } - if (col - 1 >= 0 && m[row][col - 1] == 1) { - String left = String.valueOf(row) + "_" + String.valueOf(col - 1); - unionSet.union(left, position); - } - } - } + us.Nodes = nodes + us.RootFatherMap = fatherMap + us.SizeMap = sizeMap + return us +} + +// FindFather 在并查集结构中找一个节点的父亲根节点 +// 从点cur开始,一直往上找,找到不能再往上的代表点,返回 +// 通过把路径上所有节点指向最上方的代表节点,目的是把findFather优化成O(1)的 +func (set *UnionSet) FindFather(cur *UNode) *UNode { + // 在找father的过程中,沿途所有节点加入当前容器,便于后面扁平化处理 + path := make([]*UNode, 0) + // 当前节点的父亲不是指向自己,进行循环 + for cur != set.RootFatherMap[cur] { + path = append(path, cur) + // 向上移动 + cur = set.RootFatherMap[cur] + } + // 循环结束,cur此时是最上的代表节点 + // 把沿途所有节点拍平,都指向当前最上方的代表节点 + for len(path) != 0 { + for i := len(path) - 1; i >= 0; i-- { + set.RootFatherMap[path[i]] = cur + path = path[:len(path) - 1] // 模拟栈的弹出 } - return unionSet.getSetNum(); } + return cur +} - public static void main(String[] args) { - int[][] m1 = { { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 1, 0, 1, 1, 1, 0 }, { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, - { 0, 1, 1, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, - { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, }; - System.out.println(countIslands1(m1)); - - int[][] m1Other = { { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 1, 0, 1, 1, 1, 0 }, { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, - { 0, 1, 1, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, - { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, }; - System.out.println(countIslands2(m1Other)); +// IsSameSet 判断两个元素是否在同一个并查集中 +func (set *UnionSet) IsSameSet(a, b string) bool { + // 先检查a和b有没有登记 + if _, ok := set.Nodes[a]; !ok { + return false + } + if _, ok := set.Nodes[b]; !ok { + return false + } - int[][] m2 = { { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 1, 1, 1, 1, 1, 0 }, { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, - { 0, 1, 1, 0, 0, 0, 1, 1, 0 }, { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, - { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, }; - System.out.println(countIslands1(m2)); + // 比较a的最上的代表点和b最上的代表点 + return set.FindFather(set.Nodes[a]) == set.FindFather(set.Nodes[b]) +} - int[][] m2Other = { { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 1, 1, 1, 1, 1, 0 }, { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, - { 0, 1, 1, 0, 0, 0, 1, 1, 0 }, { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, - { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, }; +// Union 合并两个元素 +func (set *UnionSet) Union(a, b string) { + // 先检查a和b有没有都登记过 + if _, ok := set.Nodes[a]; !ok { + return + } + if _, ok := set.Nodes[b]; !ok { + return + } - System.out.println(countIslands2(m2Other)); + // 找到a的最上面的代表点 + aHead := set.FindFather(set.Nodes[a]) + // 找到b的最上面的代表点 + bHead := set.FindFather(set.Nodes[b]) + // 只有两个最上代表点内存地址不相同,需要union + if aHead != bHead { + // 由于aHead和bHead都是最上面的代表点,那么在sizeMap里可以拿到大小 + aSetSize := set.SizeMap[aHead] + bSetSize := set.SizeMap[bHead] + var big *UNode + var small *UNode + // 哪个小,哪个挂在下面 + if aSetSize >= bSetSize { + big = aHead + small = bHead + } else { + big = bHead + small = aHead + } + // 把小集合直接挂到大集合的最上面的代表节点下面 + set.RootFatherMap[small] = big + // 大集合的代表节点的size要吸收掉小集合的size + set.SizeMap[big] = aSetSize + bSetSize + // 把被吸收掉的小set删除掉 + delete(set.SizeMap, small) } +} +func (set *UnionSet) getSetNum() int { + return len(set.SizeMap) } ``` From 3366072846ce99dad999850b4494daccd2493546 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Tue, 5 Apr 2022 14:00:54 +0800 Subject: [PATCH 12/18] linkedlist refactor --- ...3\351\242\230-\351\223\276\350\241\250.md" | 679 +++++++++--------- 1 file changed, 323 insertions(+), 356 deletions(-) diff --git "a/25-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\351\223\276\350\241\250.md" "b/25-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\351\223\276\350\241\250.md" index 6786fdb..88070c6 100644 --- "a/25-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\351\223\276\350\241\250.md" +++ "b/25-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\351\223\276\350\241\250.md" @@ -1,358 +1,325 @@ -```java -package com.xiaodai.algorithm; - -/** - * Author :dai - * Date :2020/12/25 5:04 下午 - * Description: - */ -public class LinkedListUtil { - - - /** - * 链表的节点,可实现成泛型 - */ - public static class Node { - public int value; - public Node next; - - public Node(int data) { - value = data; - } - } - - /** - * 双向列表的节点结构,可实现成泛型 - */ - public static class DoubleNode { - public int value; - public DoubleNode last; - public DoubleNode next; - - public DoubleNode(int data) { - value = data; - } - } - - - /** - * 1、检测链表是否成环。返回成环是否,第一次相遇并不保证是成环的节点 - * - * @param head - * @return - */ - public boolean hasCycle(Node head) { - - if (head == null || head.next == null) { - return false; - } - - Node slow = head; - Node fast = head.next; - - while (slow != fast) { - if (fast == null || fast.next == null) { - return false; - } - - slow = slow.next; - fast = fast.next.next; - } - - // 有环的话一定追的上,但不一定是第一次成环的节点 - return true; - } - - - /** - * 2、传入头节点,翻转单项链表 - * - * @param head - * @return - */ - public static Node reverseLinkedList(Node head) { - Node pre = null; - Node next = null; - while (head != null) { - next = head.next; - head.next = pre; - pre = head; - head = next; - } - return pre; - } - - /** - * 3、移除链表中等于值的节点 - *

- * 例如:1->2->3->3->4->5->3, 和 val = 3, 你需要返回删除3之后的链表:1->2->4->5。 - * - * @param head - * @param num - * @return - */ - public static Node removeValue(Node head, int num) { - - // 从链表的头开始,舍弃掉开头的且连续的等于num的节点 - while (head != null) { - if (head.value != num) { - break; - } - head = head.next; - } - - // head来到 第一个不需要删的位置 - Node pre = head; - Node cur = head; - - // 快慢指针 - while (cur != null) { - if (cur.value == num) { // 快指针cur向下滑动,如果值等于num,则暂时把下一个节点给慢指针的下一个指向。从而跳过等于num的节点 - pre.next = cur.next; - } else { // cur此时到了不等于num的节点,则慢指针追赶上去。达到的效果就是等于num的节点都被删掉了 - pre = cur; - } - // 快指针向下滑动 - cur = cur.next; - } - return head; - } - - /** - * 4、打印两个有序链表的公共部分 - * 例如:head1: 1->2->3->3->4->5 head2: 0->0->1->2->3->3->7->9 - * 公共部分为:1 2 3 3 - * - * @param head1 - * @param head2 - */ - public void printCommonPart(Node head1, Node head2) { - - System.out.println("Common Part: "); - - while (head1 != null && head2 != null) { - if (head1.value < head2.value) { - head1 = head1.next; - } else if (head1.value > head2.value) { - head2 = head2.next; - } else { - System.out.println(head1.value); - head1 = head1.next; - head2 = head2.next; - } - } - System.out.println(); - } - - /** - * 5、删除单链表的倒数第k个节点 - * - * @param head - * @param lastKth - * @return - */ - public Node removeLastKthNode(Node head, int lastKth) { - if (head == null || lastKth < 1) { - return head; - } - - // cur指针也指向链表头节点 - Node cur = head; - // 检查倒数第lastKth个节点的合法性 - while (cur != null) { - lastKth--; - cur = cur.next; - } - - // 需要删除的是头结点 - if (lastKth == 0) { - head = head.next; - } - - if (lastKth < 0) { - // cur回到头结点 - cur = head; - while (++lastKth != 0) { - cur = cur.next; - } - // 次吃cur就是要删除的前一个节点。把原cur.next删除 - cur.next = cur.next.next; - } - - // lastKth > 0的情况,表示倒数第lastKth节点比原链表程度要大,即不存在 - return head; - } - - /** - * 6、删除链表中间节点 - * 思路:如果链表为空或者只有一个节点,不做处理。链表两个节点删除第一个节点,链表三个节点,删除中间第二个节点,链表四个节点,删除上中点 - * - * @param head - * @return - */ - public Node removeMidNode(Node head) { - // 无节点,或者只有一个节点的情况,直接返回 - if (head == null || head.next == null) { - return head; - } - - // 链表两个节点,删除第一个节点 - if (head.next.next == null) { - return head.next; - } - - Node pre = head; - Node cur = head.next.next; - - // 快慢指针 - if (cur.next != null && cur.next.next != null) { - pre = pre.next; - cur = cur.next.next; - } - - // 快指针走到尽头,慢指针奇数长度停留在中点,偶数长度停留在上中点。删除该节点 - pre.next = pre.next.next; - - return head; - } - - /** - * 7、给定一个链表,如果成环,返回成环的那个节点 - *

- * 思路: - * 1. 快慢指针fast和slow,开始时,fast和slow都指向头节点,fast每次走两步,slow每次走一步 - * 2. 快指针向下移动的过程中,如果提前到达null,则链表无环,提前结束 - * 3. 如果该链表成环,那么fast和slow一定在环中的某个位置相遇 - * 4. 相遇后,立刻让fast回到head头结点,slow不动,fast走两步改为每次走一步。fast和slow共同向下滑动,再次相遇,就是成环节点 - * - * @param head - * @return - */ - public Node getLoopNode(Node head) { - // 节点数目不足以成环,返回不存在成环节点 - if (head == null || head.next == null || head.next.next == null) { - return null; - } - - Node n1 = head.next; // slow指针 - Node n2 = head.next.next; // fast指针 - - while (n1 != n2) { - // 快指针提前到达终点,该链表无环 - if (n2.next == null || n2.next.next == null) { - return null; - } - - n2 = n2.next.next; - n1 = n1.next; - } - - // 确定成环,n2回到头节点 - n2 = head; - - while (n1 != n2) { - n2 = n2.next; - n1 = n1.next; - } - - // 再次相遇节点,就是成环节点 - return n1; - } - - /** - * 由于单链表,两个链表相交要不然两个无环链表相交,最后是公共部分;要不然两个链表相交,最后是成环部分 - *

- * 8、判断两个无环链表是否相交,相交则返回相交的第一个节点 - *

- * 思路: - * 1. 链表1从头结点遍历,统计长度,和最后节点end1 - * 2. 链表2从头结点遍历,统计长度,和最后节点end2 - * 3. 如果end1不等一end2则一定不相交,如果相等则相交,算长度差,长的链表遍历到长度差的长度位置,两个链表就汇合在该位置 - * - * @param head1 - * @param head2 - * @return - */ - public Node noLoop(Node head1, Node head2) { - if (head1 == null || head2 == null) { - return null; - } - - Node cur1 = head1; - Node cur2 = head2; - int n = 0; - - while (cur1.next != null) { - n++; - cur1 = cur1.next; - } - - while (cur2.next != null) { - n--; - cur2 = cur2.next; - } - - // 最终没汇聚,说明两个链表不相交 - if(cur1 != cur2) { - return null; - } - - cur1 = n > 0 ? cur1 : cur2; - cur2 = cur1 == head1 ? head2 : head1; - n = Math.abs(n); - - while (n != 0) { - n--; - cur1 = cur1.next; - } - - while (cur1 != cur2) { - cur1 = cur1.next; - cur2 = cur2.next; - } - - return cur1; - - } - - /** - * 9、合并两个有序链表 - * @param head1 - * @param head2 - * @return - */ - public Node mergeTwoList(Node head1, Node head2) { - // base case - if (head1 == null || head2 == null) { - return head1 == null ? head2 : head1; - } - - // 选出两个链表较小的头作为整个合并后的头结点 - Node head = head1.value <= head2.value ? head1 : head2; - // 链表1的准备合并的节点,就是头结点的下一个节点 - Node cur1 = head.next; - // 链表2的准备合并的节点,就是另一个链表的头结点 - Node cur2 = head == head1 ? head2 : head1; - - // 最终要返回的头结点,预存为head,使用引用拷贝的pre向下移动 - Node pre = head; - while (cur1 != null && cur2 != null) { - if (cur1.value <= cur2.value) { - pre.next = cur1; - // 向下滑动 - cur1 = cur1.next; - } else { - pre.next = cur2; - // 向下滑动 - cur2 = cur2.next; - } - // pre向下滑动 - pre = pre.next; - } - - // 有一个链表耗尽了,没耗尽的链表直接拼上 - pre.next = cur1 != null ? cur1 : cur2; - return head; - } +```Go +package main + +import ( + "fmt" + "math" +) + +// Node 链表的节点结构 +type Node struct { + Value int + Next *Node +} + +// DuLNode 双向链表的节点结构 +type DuLNode struct { + Value int + Pre *DuLNode + Next *DuLNode +} + +// 1、检测链表是否成环。返回成环是否,第一次相遇并不保证是成环的节点 +func hasCycle(head *Node) bool { + if head == nil || head.Next == nil { + return false + } + + slow := head + fast := head.Next + + for slow != fast { + if fast == nil || fast.Next == nil { + return false + } + + slow = slow.Next + fast = fast.Next.Next + } + + // 有环的话一定追的上,但不一定是第一次成环的节点 + return true +} + +// 2、传入头节点,翻转单项链表。返回翻转后的新的头节点 +func reverseLinkedList(head *Node) *Node { + var pre *Node + var next *Node + + for head != nil { + next = head.Next + head.Next = pre + pre = head + head = next + } + + return pre +} + +// 3、移除链表中等于值的节点。返回处理后的头结点 +// 例如:1->2->3->3->4->5->3, 和 val = 3, 你需要返回删除3之后的链表:1->2->4->5。 +func removeValue(head *Node, num int) *Node { + + // 从链表的头开始,舍弃掉开头的且连续的等于num的节点 + for head != nil { + if head.Value != num { + break + } + head = head.Next + } + + if head == nil { // 头结点处理完毕,发现全部都等于num的情况。 + return head + } + + // head来到 第一个不需要删的位置 + pre := head + cur := head + + // 快慢指针 + for cur != nil { + if cur.Value == num { // 快指针cur向下滑动,如果值等于num,则暂时把下一个节点给慢指针的下一个指向。从而跳过等于num的节点 + pre.Next = cur.Next + } else { // cur此时到了不等于num的节点,则慢指针追赶上去。达到的效果就是等于num的节点都被删掉了 + pre = cur + } + + // 快指针向下滑动 + cur = cur.Next + } + return head +} + +// 4、打印两个有序链表的公共部分 +// 例如:head1: 1->2->3->3->4->5 head2: 0->0->1->2->3->3->7->9 +// 公共部分为:1 2 3 3 +func printCommonPart(head1, head2 *Node) { + fmt.Println("Common Part: ") + + for head1 != nil && head2 != nil { + if head1.Value < head2.Value { + head1 = head1.Next + } else if head1.Value > head2.Value { + head2 = head2.Next + } else { + fmt.Println(head1.Value) + head1 = head1.Next + head2 = head2.Next + } + } + fmt.Println() +} + +// 5、删除单链表的倒数第k个节点 +func removeLastKthNode(head *Node, lastKth int) *Node { + if head == nil || lastKth < 1 { + return head + } + + // cur指针也指向链表头节点 + cur := head + // 检查倒数第lastKth个节点的合法性 + for cur != nil { + lastKth-- + cur = cur.Next + } + + // 需要删除的是头结点 + if lastKth == 0 { + head = head.Next + } + + if lastKth < 0 { + // cur回到头结点 + cur = head + for lastKth != 0 { + lastKth++ + cur = cur.Next + } + + // 此次cur就是要删除的前一个节点。把原cur.next删除 + cur.Next = cur.Next.Next + } + + // lastKth > 0的情况,表示倒数第lastKth节点比原链表程度要大,即不存在 + return head +} + +// 6、删除链表中间节点 +// 思路:如果链表为空或者只有一个节点,不做处理。链表两个节点删除第一个节点,链表三个节点,删除中间第二个节点,链表四个节点,删除上中点 +func removeMidNode(head *Node) *Node { + // 无节点,或者只有一个节点的情况,直接返回 + if head == nil || head.Next == nil { + return head + } + + // 链表两个节点,删除第一个节点 + if head.Next.Next == nil { + // free first node mem + return head.Next + } + + pre := head + cur := head.Next.Next + + // 快慢指针 + if cur.Next != nil && cur.Next.Next != nil { + pre = pre.Next + cur = cur.Next.Next + } + + // 快指针走到尽头,慢指针奇数长度停留在中点,偶数长度停留在上中点。删除该节点 + pre.Next = pre.Next.Next + return head +} + +// 7、给定一个链表,如果成环,返回成环的那个节点 +// 思路: +// 1. 快慢指针fast和slow,开始时,fast和slow都指向头节点,fast每次走两步,slow每次走一步 +// 2. 快指针向下移动的过程中,如果提前到达null,则链表无环,提前结束 +// 3. 如果该链表成环,那么fast和slow一定在环中的某个位置相遇 +// 4. 相遇后,立刻让fast回到head头结点,slow不动,fast走两步改为每次走一步。fast和slow共同向下滑动,再次相遇,就是成环节点 + +func getLoopNode(head *Node) *Node { + // 节点数目不足以成环,返回不存在成环节点 + if head == nil || head.Next == nil || head.Next.Next == nil { + return nil + } + + n1 := head.Next // slow指针 + n2 := head.Next.Next // fast指针 + + for n1 != n2 { + // 快指针提前到达终点,该链表无环 + if n2.Next == nil || n2.Next.Next == nil { + return nil + } + + n2 = n2.Next.Next + n1 = n1.Next + } + + // 确定成环,n2回到头节点 + n2 = head + + for n1 != n2 { + n2 = n2.Next + n1 = n1.Next + } + + // 再次相遇节点,就是成环节点 + return n1 +} + + +// 8、判断两个无环链表是否相交,相交则返回相交的第一个节点 +// 由于单链表,两个链表相交要不然两个无环链表相交,最后是公共部分;要不然两个链表相交,最后是成环部分. +// 思路: +// 1. 链表1从头结点遍历,统计长度,和最后节点end1 +// 2. 链表2从头结点遍历,统计长度,和最后节点end2 +// 3. 如果end1不等一end2则一定不相交,如果相等则相交,算长度差,长的链表遍历到长度差的长度位置,两个链表就汇合在该位置 +func noLoop(head1, head2 *Node) *Node { + if head1 == nil || head2 == nil { + return nil + } + + cur1 := head1 + cur2 := head2 + n := 0 + + for cur1.Next != nil { + n++ + cur1 = cur1.Next + } + + for cur2.Next != nil { + n-- + cur2 = cur2.Next + } + + // 最终没汇聚,说明两个链表不相交 + if cur1 != cur2 { + return nil + } + + if n <= 0 { + cur1 = cur2 + } + + if cur1 == head1 { + cur2 = head2 + } else { + cur2 = head1 + } + + n = int(math.Abs(float64(n))) + + for n != 0 { + n-- + cur1 = cur1.Next + } + + for cur1 != cur2 { + cur1 = cur1.Next + cur2 = cur2.Next + } + + return cur1 +} + +// 9、合并两个有序链表 +func mergeTwoList(head1, head2 *Node) *Node { + // base case + if head1 == nil { + return head2 + } + if head2 == nil { + return head1 + } + + var head *Node + + // 选出两个链表较小的头作为整个合并后的头结点 + if head1.Value <= head2.Value { + head = head1 + } else { + head = head2 + } + + // 链表1的准备合并的节点,就是头结点的下一个节点 + cur1 := head.Next + // 链表2的准备合并的节点,就是另一个链表的头结点 + var cur2 *Node + if head == head1 { + cur2 = head2 + } else { + cur2 = head1 + } + + // 最终要返回的头结点,预存为head,使用引用拷贝的pre向下移动 + pre := head + for cur1 != nil && cur2 != nil { + if cur1.Value <= cur2.Value { + pre.Next = cur1 + // 向下滑动 + cur1 = cur1.Next + } else { + pre.Next = cur2 + // 向下滑动 + cur2 = cur2.Next + } + + // pre向下滑动 + pre = pre.Next + } + + // 有一个链表耗尽了,没耗尽的链表直接拼上 + if cur1 != nil { + pre.Next = cur1 + } else { + pre.Next = cur2 + } + + return head } ``` \ No newline at end of file From cb2d550452ed640f5fe57915dc73407fbc2e98e5 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Tue, 5 Apr 2022 17:17:51 +0800 Subject: [PATCH 13/18] tree interview --- ...0-\344\272\214\345\217\211\346\240\221.md" | 1043 +++++++---------- 1 file changed, 409 insertions(+), 634 deletions(-) diff --git "a/26-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\344\272\214\345\217\211\346\240\221.md" "b/26-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\344\272\214\345\217\211\346\240\221.md" index ba96c1b..d367b0c 100644 --- "a/26-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\344\272\214\345\217\211\346\240\221.md" +++ "b/26-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\344\272\214\345\217\211\346\240\221.md" @@ -1,676 +1,451 @@ -- 基础 - -```java -package com.xiaodai.algorithm; - -import java.util.LinkedList; -import java.util.Queue; -import java.util.Stack; - -/** - * Author :dai - * Date :2020/12/25 2:56 下午 - * Description:二叉树基本结构和算法 - */ -public class TreeBaseUtil { - - /** - * 二叉树节点定义 - */ - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int v) { - this.value = v; - } - } +- 二叉树基础 +```Go +package main - /** - * 递归先序遍历 - * - * @param head - */ - public static void pre(Node head) { - - if (head == null) { - return; - } - - System.out.println(head.value); - - pre(head.left); - - pre(head.right); - - } - - /** - * 递归中序遍历 - * - * @param head - */ - public static void mid(Node head) { - if (head == null) { - return; - } - - mid(head.left); - - System.out.println(head.value); - - mid(head.right); - } - - /** - * 递归后续遍历 - * - * @param head - */ - public static void pos(Node head) { - if (head == null) { - return; - } - - pos(head.left); - - pos(head.right); - - System.out.println(head.value); - } - - /** - * 非递归先序 - * - * @param head - */ - public static void NotRRre(Node head) { - - System.out.print("pre-order: "); - - if (head != null) { - // 借助栈结构,手动压栈 - Stack stack = new Stack<>(); - stack.add(head); - - while (!stack.isEmpty()) { - // 弹出就打印 - head = stack.pop(); - System.out.println(head.value); - - // 右孩子不为空,先压入右孩子。右孩子就会后弹出 - if (head.right != null) { - stack.push(head.right); - } - - // 左孩子不为空,压入左孩子,左孩子在右孩子之后压栈,先弹出 - if (head.left != null) { - stack.push(head.left); - } - } - - } - - } - - /** - * 非递归中序 - * - * @param head - */ - public static void NotRMid(Node head) { - - System.out.print("mid-order: "); - - if (head != null) { - Stack stack = new Stack<>(); - while (!stack.isEmpty() || head != null) { - - // 整条左边界以此入栈 - if (head != null) { - stack.push(head); - // head滑到自己的左孩子,左孩子有可能为空,但空的节点不会加入栈,下一个分支会判空处理 - head = head.left; - // 左边界到头弹出一个打印,来到该节点右节点,再把该节点的左树以此进栈 - } else {// head为空的情况,栈顶是上次头结点的现场,head等于栈顶,回到上一个现场。打印后,head往右树上滑动 - head = stack.pop(); - System.out.println(head.value); - head = head.right; - } - } - } - - } - - - /** - * 非递归后序,借助两个栈,比借助一个栈容易理解 - * - * @param head - */ - public static void NotRPos(Node head) { - System.out.print("pos-order: "); - - if (head != null) { - Stack s1 = new Stack<>(); - // 辅助栈 - Stack s2 = new Stack<>(); - s1.push(head); - while (!s1.isEmpty()) { - head = s1.pop(); - s2.push(head); - if (head.left != null) { - s1.push(head.left); - } - if (head.right != null) { - s1.push(head.right); - } - } - while (!s2.isEmpty()) { - System.out.print(s2.pop().value + " "); - } - } - System.out.println(); - } - - - /** - * 非递归后序,仅借助一个栈,比较有技巧 - * - * @param head - */ - public static void NotRPos2(Node head) { - System.out.print("pos-order: "); - if (head != null) { - Stack stack = new Stack<>(); - stack.push(head); - Node c = null; - while (!stack.isEmpty()) { - c = stack.peek(); - if (c.left != null && head != c.left && head != c.right) { - stack.push(c.left); - } else if (c.right != null && head != c.right) { - stack.push(c.right); - } else { - System.out.print(stack.pop().value + " "); - head = c; - } - } - } - System.out.println(); - } - - - /** - * 按层遍历,即宽度优先遍历 - * - * @param head - */ - public static void level(Node head) { - - if (head == null) { - return; - } - - // 借助队列 - Queue queue = new LinkedList<>(); - - queue.add(head); - - while (!queue.isEmpty()) { - - Node cur = queue.poll(); - // 打印当前 - System.out.println(cur.value); - - // 当前节点有左孩子,加入左孩子进队列 - if (cur.left != null) { - queue.add(cur.left); - } - - // 当前节点有右孩子,加入右孩子进队列 - if (cur.right != null) { - queue.add(cur.right); - } - } - - } - - - /** - * 二叉树先序序列化;除了先序,中序,后续,按层都可,但是序列化和反序列化规则要对应 - * @param head - * @return - */ - public static Queue preSerial(Node head) { - Queue ans = new LinkedList<>(); - - pres(head, ans); - - return ans; - - } - - private static void pres(Node head, Queue ans) { - - if (head == null) { - ans.add(null); - } else { - ans.add(String.valueOf(head.value)); - pres(head.left, ans); - pres(head.right, ans); - } - } - - - /** - * 根据先序序列化的结构,还原树 - * - * @param prelist - * @return - */ - public static Node buildByPreQueue(Queue prelist) { - - if (prelist == null || prelist.size() == 0) { - return null; - } - return preb(prelist); - } - - public static Node preb(Queue prelist) { - String value = prelist.poll(); - // 如果头节点是空的话,返回空 - if (value == null) { - return null; - } - // 否则根据第一个值构建先序的头结点 - Node head = new Node(Integer.valueOf(value)); - // 递归建立左树 - head.left = preb(prelist); - // 递归建立右树 - head.right = preb(prelist); - return head; - } +import ( + "fmt" + "strconv" +) + +// Node 二叉树的节点定义 +type Node struct { + Value int + Left *Node + Right *Node } -``` -- 二叉树相关练习 +// 1、递归先序遍历 +func pre(head *Node) { + if head == nil { + return + } + + fmt.Println(head.Value) + pre(head.Left) + pre(head.Right) +} + +// 2、递归中序遍历 +func mid(head *Node) { + if head == nil { + return + } + + mid(head.Left) + fmt.Println(head.Value) + mid(head.Right) +} + +// 3、递归后序遍历 +func pos(head *Node) { + if head == nil { + return + } + + pos(head.Left) + pos(head.Right) + fmt.Println(head.Value) +} -```java -package com.xiaodai.algorithm; +// 4、非递归先序 +func notRPre(head *Node) { + fmt.Println("pre-order: ") + + if head != nil { + // 借助栈结构,手动压栈 + stack := make([]*Node, 0) + stack = append(stack, head) + + for len(stack) != 0 { + // 弹出就打印 + head = stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + + fmt.Println(head.Value) + + // 右孩子不为空,先压入右孩子。右孩子就会后弹出 + if head.Right != nil { + stack = append(stack, head.Right) + } + + // 左孩子不为空,压入左孩子,左孩子在右孩子之后压栈,先弹出 + if head.Left != nil { + stack = append(stack, head.Left) + } + } + } +} -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.Queue; +// 5、非递归中序 +func notRMid(head *Node) { + fmt.Println("mid-order: ") + + if head != nil { + stack := make([]*Node, 0) + + for len(stack) != 0 || head != nil { + // 整条左边界依次入栈 + if head != nil { + stack = append(stack, head) + // head滑到自己的左孩子,左孩子有可能为空,但空的节点不会加入栈,下一个分支会判空处理 + head = head.Left + // 左边界到头弹出一个打印,来到该节点右节点,再把该节点的左树以此进栈 + } else { // head为空的情况,栈顶是上次头结点的现场,head等于栈顶,回到上一个现场。打印后,head往右树上滑动 + head = stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + + fmt.Println(head.Value) + head = head.Right + } + } + } +} -/** - * Author :dai - * Date :2020/12/30 2:56 下午 - * Description:解决二叉树的具体问题,递归思维的建立 - */ -public class TreeSolvingUtil { +// 6、非递归后序,借助两个栈,比借助一个栈容易理解 +func notRPos(head *Node) { + fmt.Println("pos-order: ") + + if head != nil { + stack1 := make([]*Node, 0) + stack2 := make([]*Node, 0) + + stack1 = append(stack1, head) + + for len(stack1) != 0 { + head = stack1[len(stack1) - 1] + stack1 = stack1[:len(stack1) - 1] + + stack2 = append(stack2, head) + if head.Left != nil { + stack1 = append(stack1, head.Left) + } + if head.Right != nil { + stack1 = append(stack1, head.Right) + } + } + + for len(stack2) != 0 { + cur := stack2[len(stack2) - 1] + stack2 = stack2[:len(stack2) - 1] + fmt.Println(cur.Value) + } + } + fmt.Println() +} - /** - * 节点信息 - */ - public static class Node { - public int value; - public Node left; - public Node right; +// 7、非递归后序,仅借助一个栈,比较有技巧 +func notRPos2(head *Node) { + fmt.Println("pos-order: ") + + if head != nil { + stack := make([]*Node, 0) + stack = append(stack, head) + var c *Node + + for len(stack) != 0 { + c = stack[len(stack) - 1] // stack peek + if c.Left != nil && head != c.Left && head != c.Right { + stack = append(stack, c.Left) + } else if c.Right != nil && head != c.Right { + stack = append(stack, c.Right) + } else { + stack = stack[:len(stack) - 1] // pop + fmt.Println(c.Value) + head = c + } + } + } + fmt.Println() +} - public Node(int data) { - this.value = data; - } - } +// 8、按层遍历,即宽度优先遍历 +func level(head *Node) { + if head == nil { + return + } + + queue := make([]*Node, 0) + queue = append(queue, head) + + for len(queue) != 0 { + cur := queue[0] // queue poll + queue = queue[1:] + + // 打印当前 + fmt.Println(cur.Value) + + // 当前节点有左孩子,加入左孩子进队列 + if cur.Left != nil { + queue = append(queue, cur.Left) + } + // 当前节点有右孩子,加入右孩子进队列 + if cur.Right != nil { + queue = append(queue, cur.Right) + } + } +} + +// 9、二叉树的先序序列化 +func preSerial(head *Node) []string { + ansQueue := make([]string, 0) + + pres(head, ansQueue) + return ansQueue +} +func pres(head *Node, ans []string) { + if head == nil { + ans = append(ans, "") + } else { + ans = append(ans, fmt.Sprintf("%d", head.Value)) + pres(head.Left, ans) + pres(head.Right, ans) + } +} - /** - * 1、判断二叉树是否是平衡的 - * - * @param head - * @return - */ - public static boolean isBalanced(Node head) { +// 10、根据先序序列化的结果,反序列化成一颗树 +func buildByPreQueue(prelist []string) *Node { + if len(prelist) == 0 { + return nil + } - return isBalancedProcess(head).isBalanced; + return preb(prelist) +} - } +func preb(prelist []string) *Node { + value := prelist[0] + prelist = prelist[1:] + + // 如果头节点是空的话,返回空 + if value == "" { + return nil + } + + // 否则根据第一个值构建先序的头结点 + v, _ := strconv.Atoi(value) + head := &Node{ + Value: v, + Left: nil, + Right: nil, + } + // 递归建立左树 + head.Left = preb(prelist) + // 递归建立右树 + head.Right = preb(prelist) + return head +} +``` - /** - * 1. 递归调用,head传入整体需要返回一个信息 - * 2. 解决当前节点的Info信息怎么得来 - * - * @param head - * @return - */ - private static IsBalancedInfo isBalancedProcess(Node head) { +- 二叉树应用 - if (head == null) { - return new IsBalancedInfo(true, 0); - } +```Go +package main - IsBalancedInfo leftInfo = isBalancedProcess(head.left); +import "math" - IsBalancedInfo rightInfo = isBalancedProcess(head.right); +// Node 二叉树的节点定义 +type Node struct { + Value int + Left *Node + Right *Node +} - // 当前节点的高度,等于左右树最大的高度,加上当前节点高度1 - int currentHeight = Math.max(leftInfo.height, rightInfo.height) + 1; - boolean isBalanced = true; +// IsBalanced 1、判断二叉树是否是平衡的 +func IsBalanced(head *Node) bool { + return isBalancedProcess(head).isBalanced +} - // 左树不平衡,或者右树不平衡,或者左右树高度差大于1 - if (!leftInfo.isBalanced || !rightInfo.isBalanced || Math.abs(leftInfo.height - rightInfo.height) > 1) { - isBalanced = false; - } +// 递归过程信息 +type isBalancedInfo struct { + isBalanced bool + height int +} - return new IsBalancedInfo(isBalanced, currentHeight); - } +// 递归调用,head传入整体需要返回一个信息 +// 解决当前节点的Info信息怎么得来 +func isBalancedProcess(head *Node) *isBalancedInfo { + if head == nil { + return &isBalancedInfo{ + isBalanced: true, + height: 0, + } + } + + leftInfo := isBalancedProcess(head.Left) + rightInfo := isBalancedProcess(head.Right) + + // 当前节点的高度,等于左右树最大的高度,加上当前节点高度1 + cHeight := int(math.Max(float64(leftInfo.height), float64(rightInfo.height))) + 1 + isBalanced := true + + if !leftInfo.isBalanced || !rightInfo.isBalanced || int(math.Abs(float64(leftInfo.height - rightInfo.height))) > 1 { + isBalanced = false + } + + return &isBalancedInfo{ + isBalanced: isBalanced, + height: cHeight, + } +} - /** - * 递归过程中需要整合的信息体 - */ - public static class IsBalancedInfo { +// MaxDistance 2、二叉树中,获取任意两个节点的最大距离 +func MaxDistance(head *Node) int { + return maxDistanceProcess(head).maxDistance +} - // 是否平衡 - boolean isBalanced; +type maxDistanceInfo struct { + maxDistance int + height int +} - // 高度多少 - int height; +func maxDistanceProcess(head *Node) *maxDistanceInfo { + if head == nil { + return &maxDistanceInfo{ + maxDistance: 0, + height: 0, + } + } + + leftInfo := maxDistanceProcess(head.Left) + rightInfo := maxDistanceProcess(head.Right) + + // 当前节点为头的情况下,高度等于左右树较大的高度,加上1 + height := int(math.Max(float64(leftInfo.height), float64(rightInfo.height))) + 1 + + // 当前节点为头的情况下,最大距离等于,左右树距离较大的那个距离(与当前节点无关的情况) + // 和左右树高度相加再加上当前节点距离1的距离(与当前节点有关的情况)取这两种情况较大的那个 + maxDistance := int(math.Max(math.Max(float64(leftInfo.maxDistance), float64(rightInfo.maxDistance)), + float64(leftInfo.height + rightInfo.height + 1))) + + return &maxDistanceInfo{ + maxDistance: maxDistance, + height: height, + } +} - IsBalancedInfo(boolean b, int height) { - this.isBalanced = b; - this.height = height; - } - } +// IsFull 3、判断一颗树是否是满二叉树 +func IsFull(head *Node) bool { + if head == nil { + return true + } + all := isFullProcess(head) - /** - * 2、二叉树中,获取任意两个节点的最大距离 - * - * @param head - * @return - */ - public static int maxDistance(Node head) { + return (1 << all.height) - 1 == all.nodes +} - return maxDistanceProcess(head).maxDistance; +// 判断一棵树是否是满二叉树,每个节点需要返回的信息 +type isFullInfo struct { + height int + nodes int +} - } +func isFullProcess(head *Node) *isFullInfo { + if head == nil { // base 空节点的高度为0,节点数量也0 + return &isFullInfo{ + height: 0, + nodes: 0, + } + } + + leftInfo := isFullProcess(head.Left) + rightInfo := isFullProcess(head.Right) + + // 当前节点为头的树,高度 + height := int(math.Max(float64(leftInfo.height), float64(rightInfo.height)) + 1) + // 当前节点为头的树,节点数量 + nodes := leftInfo.nodes + rightInfo.nodes + 1 + + return &isFullInfo{ + height: height, + nodes: nodes, + } +} - /** - * 任意节点需要返回的信息体:以该节点为头的高度,整棵树的最大距离 - */ - public static class MaxDistanceInfo { - public int maxDistance; - public int height; +// GetMaxLength 4、找到二叉树中节点和等于sum的最长路径 +func GetMaxLength(head *Node, sum int) int { + sumMap := make(map[int]int, 0) + sumMap[0] = 0 - public MaxDistanceInfo(int dis, int h) { - this.maxDistance = dis; - this.height = h; - } - } + return preOrder(head, sum, 0, 1, 0, sumMap) +} - private static MaxDistanceInfo maxDistanceProcess(Node head) { +func preOrder(head *Node, sum int, preSum int, level int, maxLen int, sumMap map[int]int) int { + if head == nil { + return maxLen + } - if (head == null) { - return new MaxDistanceInfo(0, 0); - } + curSum := preSum + head.Value + if _, ok := sumMap[curSum]; !ok { + sumMap[curSum] = level + } - MaxDistanceInfo leftInfo = maxDistanceProcess(head.left); + if v, ok := sumMap[curSum - sum]; ok { + maxLen = int(math.Max(float64(level - v), float64(maxLen))) + } - MaxDistanceInfo rightInfo = maxDistanceProcess(head.right); + maxLen = preOrder(head.Left, sum, curSum, level + 1, maxLen, sumMap) + maxLen = preOrder(head.Right, sum, curSum, level + 1, maxLen, sumMap) - // 当前节点为头的情况下,高度等于左右树较大的高度,加上1 - int height = Math.max(leftInfo.height, rightInfo.height) + 1; + if level == sumMap[curSum] { + delete(sumMap, curSum) + } - // 当前节点为头的情况下,最大距离等于,左右树距离较大的那个距离(与当前节点无关的情况) - // 和左右树高度相加再加上当前节点距离1的距离(与当前节点有关的情况)取这两种情况较大的那个 - int maxDistance = Math.max(Math.max(leftInfo.maxDistance, rightInfo.maxDistance) - , (leftInfo.height + rightInfo.height + 1)); + return maxLen +} - return new MaxDistanceInfo(maxDistance, height); - } +// LowestCommonAncestor 5、二叉树,给定头结点节点,及树上的两个人节点,求这两个节点的最近公共祖先 +func LowestCommonAncestor(root *Node, p *Node, q *Node) *Node { + // 如果树为空,直接返回null; + // 如果 p和q中有等于 root的,那么它们的最近公共祖先即为root(一个节点也可以是它自己的祖先) + if root == nil || p == root || q == root { + return root + } + + // 递归遍历左子树,只要在左子树中找到了p或q,则先找到谁就返回谁 + left := LowestCommonAncestor(root.Left, p, q) + // 递归遍历右子树,只要在右子树中找到了p或q,则先找到谁就返回谁 + right := LowestCommonAncestor(root.Right, p, q) + + // left和 right均不为空时,说明 p、q节点分别在 root异侧, 最近公共祖先即为 root + if left != nil && right != nil { + return root + } + + // 如果在左子树中p和q都找不到,则p和q一定都在右子树中,右子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先) + // 否则,如果 left不为空,在左子树中有找到节点(p或q),这时候要再判断一下右子树中的情况, + // 如果在右子树中,p和q都找不到,则 p和q一定都在左子树中,左子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先) + if left == nil { + return right + } else { + return left + } +} +// IsSymmetric 6、给定一个二叉树头节点,判断这颗树是否是镜面堆成的。即是否是是镜像二叉树 +func IsSymmetric(root *Node) bool { + // 自身,和自身的镜像树去递归比较 + return isMirror(root, root) +} - /** - * 3、判断一颗树是否是满二叉树 - * - * @param head - * @return - */ - public static boolean isFull(Node head) { - - if (head == null) { - return true; - } - - IsFullInfo all = isFullProcess(head); - - // 递归拿到整棵树的头结点个数,根据满二叉树的公式,验证。(高度乘以2) - 1 = 节点个数 - return (1 << all.height) - 1 == all.nodes; - } - - - /** - * 判断一棵树是否是满二叉树,每个节点需要返回的信息 - */ - public static class IsFullInfo { - public int height; - - public int nodes; - - public IsFullInfo(int height, int nodes) { - this.height = height; - this.nodes = nodes; - } - } - - private static IsFullInfo isFullProcess(Node head) { - - // base 空节点的高度为0,节点数量也0 - if(head == null) { - return new IsFullInfo(0,0); - } - - // 左树信息 - IsFullInfo leftInfo = isFullProcess(head.left); - - // 右树信息 - IsFullInfo rightInfo = isFullProcess(head.right); - - // 当前节点为头的树,高度 - int height = Math.max(leftInfo.height, rightInfo.height) + 1; - - // 当前节点为头的树,节点数量 - int nodes = leftInfo.nodes + rightInfo.nodes + 1; - - return new IsFullInfo(height, nodes); - - } - - /** - * 4、找到二叉树中节点和等于sum的最长路径 - * @param head - * @param sum - * @return - */ - public int getMaxLength(Node head, int sum) { - Map sumMap = new HashMap<>(); - sumMap.put(0, 0); // 重要 - return preOrder(head, sum, 0, 1, 0, sumMap); - } - - private int preOrder(Node head, int sum, int preSum, int level, int maxLen, Map sumMap) { - if(head == null) { - return maxLen; - } - - int curSum = preSum + head.value; - if(!sumMap.containsKey(curSum)) { - sumMap.put(curSum, level); - } - - if(sumMap.containsKey(curSum - sum)) { - maxLen = Math.max(level - sumMap.get(curSum - sum), maxLen); - } - maxLen = preOrder(head.left, sum, curSum, level + 1, maxLen, sumMap); - maxLen = preOrder(head.right, sum, curSum, level + 1, maxLen, sumMap); - - if(level == sumMap.get(curSum)) { - sumMap.remove(curSum); - } - - return maxLen; - } - - /** - * 5、二叉树按层打印 - * - * last:表示正在打印的当前行的最右节点 - * nLast:表示下一行的最右节点 - */ - public void printByLevel(Node head) { - if(head == null) { - return; - } - - Queue queue = new LinkedList<>(); - int level = 1; - Node last = head; - Node nLast = null; - queue.offer(head); - System.out.println("Level " + (level++) + " : "); - while (!queue.isEmpty()) { - head = queue.poll(); - System.out.println(head.value + " "); - if(head.left != null) { - queue.offer(head.left); - nLast = head.left; - } - if(head.right != null) { - queue.offer(head.right); - nLast = head.right; - } - if(head == last && !queue.isEmpty()) { - System.out.println("\nLevel " + (level++) + " "); - last = nLast; - } - } - System.out.println(); - } - - - /** - * 6、二叉树,zigzag打印 - * - * @param root 根节点 - * @return - */ - - public static List> zigzagLevelOrder(TreeNode root) { - List> ans = new ArrayList<>(); - if (root == null) { - return ans; - } - LinkedList deque = new LinkedList<>(); - deque.add(root); - int size = 0; - // 开关每次切换,打印顺序每次变化 - boolean isHead = true; - while (!deque.isEmpty()) { - size = deque.size(); - List curLevel = new ArrayList<>(); - for (int i = 0; i < size; i++) { - TreeNode cur = isHead ? deque.pollFirst() : deque.pollLast(); - curLevel.add(cur.val); - if(isHead) { - if (cur.left != null) { - deque.addLast(cur.left); - } - if (cur.right != null) { - deque.addLast(cur.right); - } - }else { - if (cur.right != null) { - deque.addFirst(cur.right); - } - if (cur.left != null) { - deque.addFirst(cur.left); - } - } - } - ans.add(curLevel); - isHead = !isHead; - } - return ans; - } - - /** - * 7、二叉树,给定量给节点,求这两个节点的最近公共祖先 - * - * @param root 根节点 - * @param p p节点 - * @param q q节点 - * @return - */ - public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - - // 如果树为空,直接返回null; - // 如果 p和q中有等于 root的,那么它们的最近公共祖先即为root(一个节点也可以是它自己的祖先) - if (root == null || p == root || q == root) { - return root; - } - - - // 递归遍历左子树,只要在左子树中找到了p或q,则先找到谁就返回谁 - TreeNode left = lowestCommonAncestor(root.left, p, q); - // 递归遍历右子树,只要在右子树中找到了p或q,则先找到谁就返回谁 - TreeNode right = lowestCommonAncestor(root.right, p, q); - - - // left和 right均不为空时,说明 p、q节点分别在 root异侧, 最近公共祖先即为 root - if (left != null && right != null) { - return root; - } - - // 如果在左子树中p和q都找不到,则p和q一定都在右子树中,右子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先) - // 否则,如果 left不为空,在左子树中有找到节点(p或q),这时候要再判断一下右子树中的情况, - // 如果在右子树中,p和q都找不到,则 p和q一定都在左子树中,左子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先) - return left == null ? right : left; - } - - - /** - * 8、给定一个二叉树头节点,判断这颗树是否是镜面堆成的。即是否是是镜像二叉树 - * - */ - public boolean isSymmetric(TreeNode root) { - // 自身,和自身的镜像树去递归比较 - return isMirror(root, root); - } - - // 一棵树是原始树 head1 - // 另一棵是翻面树 head2 - public static boolean isMirror(TreeNode head1, TreeNode head2) { - // base case 当前镜像的节点都为空,也算合法的镜像 - if (head1 == null && head2 == null) { - return true; - } - // 互为镜像的两个点不为空 - if (head1 != null && head2 != null) { - // 当前两个镜像点要是相等的, - // A树的左树和B树的右树互为镜像且满足,且A树的右树和B树的左树互为镜像,且满足。 - // 那么当前的镜像点下面的都是满足的 - return head1.val == head2.val - && isMirror(head1.left, head2.right) - && isMirror(head1.right, head2.left); - } - // 一个为空,一个不为空 肯定不构成镜像 false - return false; - } - - +// 一棵树是原始树 head1 +// 另一棵是翻面树 head2 +func isMirror(head1, head2 *Node) bool { + // base case 当前镜像的节点都为空,也算合法的镜像 + if head1 == nil && head2 == nil { + return true + } + + // 互为镜像的两个点不为空 + if head1 != nil && head2 != nil { + // 当前两个镜像点要是相等的, + // A树的左树和B树的右树互为镜像且满足,且A树的右树和B树的左树互为镜像,且满足。 + // 那么当前的镜像点下面的都是满足的 + return head1.Value == head2.Value && isMirror(head1.Left, head2.Right) && isMirror(head1.Right, head2.Left) + } + // 一个为空,一个不为空 肯定不构成镜像 false + return false } ``` \ No newline at end of file From 93c01f71e6187543c68b440fc5f38c076374f9b5 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Tue, 5 Apr 2022 21:24:22 +0800 Subject: [PATCH 14/18] string interview --- ...0-\345\255\227\347\254\246\344\270\262.md" | 570 ++++++++---------- 1 file changed, 256 insertions(+), 314 deletions(-) diff --git "a/27-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\255\227\347\254\246\344\270\262.md" "b/27-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\255\227\347\254\246\344\270\262.md" index 6c4770b..4520143 100644 --- "a/27-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\255\227\347\254\246\344\270\262.md" +++ "b/27-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\255\227\347\254\246\344\270\262.md" @@ -1,318 +1,260 @@ -```java -package com.xiaodai.algorithm; - -/** - * Author :dai - * Date :2021/3/17 2:01 下午 - * Description: - */ -public class StringUtil { - - /** - * 1、 判断两个字符串是否互为变形词 - * - * @param str1 - * @param str2 - * @return - */ - public boolean isDeformation(String str1, String str2) { - - if (str1 == null || str2 == null || str1.length() != str2.length()) { - return false; - } - - char[] chars1 = str1.toCharArray(); - char[] chars2 = str2.toCharArray(); - - // 字符词频统计表 - int[] map = new int[256]; - - // 对第一个字符串中的字符进行词频统计 - for (char c : chars1) { - map[c]++; - } - - // 用第二个字符串的字符去消除词频 - for (char c : chars2) { - if (map[c]-- == 0) { - return false; - } - } - - return true; - } - - - /** - * 2、 移除字符串中连续出现k个0的子串 - * - * @param str - * @param k - * @return - */ - public String removeKZeros(String str, int k) { - if (str == null || k < 1) { - return str; - } - - char[] chars = str.toCharArray(); - int count = 0, start = -1; - for (int i = 0; i < chars.length; i++) { - if (chars[i] == '0') { - count++; - start = start == -1 ? i : start; - } else { - // 如果不等于'0'需要从start位置开始,去掉count个'0'字符 - if (count == k) { - while (count-- != 0) { - // ascii码空白字符的表示为十进制的0。chars[1] = 0 表示把1位置的字符,替换为空白符 - chars[start++] = 0; - } - } - // 一轮剔除结束,count和start归位 - count = 0; - start = -1; - } - } - - // 最后一轮,即如果字符串是以'0'字符结尾的。最后要单独结算一次 - if (count == k) { - while (count-- != 0) { - chars[start++] = 0; - } - } - - return String.valueOf(chars); - } - - - /** - * 3、返回一个字符串的字符统计串 - * @param str - * @return - */ - public String getCountString(String str) { - if(str == null || str.equals("")) { - return ""; - } - - char[] chars = str.toCharArray(); - String res = String.valueOf(chars[0]); - int num = 1; - for (int i = 1; i < chars.length; i++) { - // 结算 - if(chars[i] != chars[i-1]) { - res = concat(res, String.valueOf(num), String.valueOf(chars[i])); - num = 1; - } else { - num++; - } - } - return concat(res, String.valueOf(num), ""); - } - - private String concat(String s1, String s2, String s3) { - return s1 + "_" + s2 + (s3.equals("") ? s3 : "_" + s3); - } - - /** - * 4、判断字符数组中,是否所有的字符均出现一次 - * @param chars - * @return - */ - public boolean isUnique(char[] chars) { - if(chars == null) { - return true; - } - - boolean[] map = new boolean[256]; - for (int i = 0; i < chars.length; i++) { - if(map[chars[i]]) { - return false; - } - map[chars[i]] = true; - } - return true; - } - - - /** - * 5、括号字符匹配问题:输入一个字符串,包含'(','[','{',')',']','}'几种括号,求是否是括号匹配的结果。 - * @param s - * @return - */ - public static boolean isValid(String s) { - - if (s == null || s.length() == 0) { - return true; - } - - char[] str = s.toCharArray(); - Stack stack = new Stack<>(); - - for (int i = 0; i < str.length; i++) { - char cha = str[i]; - // 遇到左括号,添加相应的右括号 - if (cha == '(' || cha == '[' || cha == '{') { - stack.add(cha == '(' ? ')' : (cha == '[' ? ']' : '}')); - } else { // 遇到右括号,弹出栈,比对相等 - if (stack.isEmpty()) { - return false; - } - char last = stack.pop(); - if (cha != last) { - return false; - } - } - } - - // 遍历结束,栈刚好为空。满足匹配要求 - return stack.isEmpty(); - } - - - /** - * 6、求一个字符串无重复最长子串 - - * 子串和子序列的区别,子串必须要连续,子序列不一定要连续。 - * 遇到子串和子序列的问题,可以按照一种经典思路: - * 按照i位置结尾的情况下答案是什么?求所有可能的结尾即可,所有位置结尾的答案都求出,最大的就是我们的目标答案 - * 时间复杂度O(N),空间复杂度O(1),由于申请的空间是固定长度256 - * @param s - * @return - */ - public static int lengthOfLongestSubstring(String s) { - // base case 过滤无效参数 - if (s == null || s.equals("")) { - return 0; - } - - char[] str = s.toCharArray(); - int[] map = new int[256]; - // 辅助数组。保存字符出现的位置,字符的范围为可显示字符0~127,扩展ascii字符128~255 - for (int i = 0; i < 256; i++) { - // 默认所有的字符都没出现过 - map[i] = -1; - } - // i位置往左推,推不动的位置第一个因素是再次遇到了i位置上的元素,第二个因素是i-1位置当初推了多远。 - // 这两个因素的限制,哪个限制位置离当前i位置近,就是当前字符i最远推到的位置,map[i] - // 收集答案。len是收集全局的最大长度 - int len = 0; - int pre = -1; // i-1位置结尾的情况下,往左推,推不动的位置是谁。用来每次保存i之前一个位置的答案 - int cur = 0; - for (int i = 0; i != str.length; i++) { - // i位置结尾的情况下,往左推,推不动的位置是谁 - // pre (i-1信息) 更新成 pre(i 结尾信息) - // 上次推不动的,和当前字符上次出现的位置map[str[i]]的位置,取大的 - pre = Math.max(pre, map[str[i]]); - // 找到了当前推不动的位置,当前不重复子串的长度就是i-pre - cur = i - pre; - // 全局最大的子串长度,是否被更新,决定是否要收集 - len = Math.max(len, cur); - // 更新当前字符出现的位置是当前位置 - map[str[i]] = i; - } - return len; - } - - - /** - * 7、最长回文子串问题。 - * 该解法是扩散法。时间复杂度为O(N * N)。(最优解是马拉车算法,可以优化该题到O(N),不掌握) - * @param s - * @return - */ - public static String longestPalindrome2(String s) { - - if(s.length() == 0) { - return s; - } - - // 全局最大回文长度 - int res = 1; - // 全局最大回文长度对应的左位置 - int ll = 0; - // 全局最大回文长度对应的右位置 - int rr = 0; - - - for (int i = 0; i < s.length(); i++) { - - // 以i为下标的奇数情况,是否有更大的len来更新res - int l = i - 1; - int r = i + 1; - - // l和r都在合法范围。且l和r位置字符相等,可以继续扩散 - while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { - int len = (r - l + 1); - // 更新最长回文串的长度 - if(len > res) { - res = len; - ll = l; - rr = r; - } - // 扩散 - l--; - r++; - } - - // 以i为下标偶数的情况。是否有更大的len来更新全局res - l = i; - r = i + 1; - // l和r都在合法范围。且l和r位置字符相等,可以继续扩散 - while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { - int len = (r - l + 1); - // 更新最长回文串的长度 - if(len > res) { - res = len; - ll = l; - rr = r; - } - // 扩散 - l--; - r++; - } - - } - - return s.substring(ll, rr + 1); - } - - - /** - * 8、字符串最长公共前缀问题 - * - **/ - public static String longestCommonPrefix(String[] strs) { - if (strs == null || strs.length == 0) { - return ""; - } - // 拿出第一个字符串。当成初始值 - char[] chs = strs[0].toCharArray(); - // 所有字符串都匹配的最大长度,等同于每个字符串和初始字符串匹配的全局最小长度 - int min = Integer.MAX_VALUE; - - for (String str : strs) { - - char[] tmp = str.toCharArray(); - int index = 0; - while (index < tmp.length && index < chs.length) { - if (chs[index] != tmp[index]) { - break; - } - index++; - } - // 更新min - min = Math.min(index, min); - // 如果有任意一个字符串和初始串不匹配,直接返回"" - if (min == 0) { - return ""; - } - } - // 截取min的长度,就是所有字符串共同匹配的最大长度 - return strs[0].substring(0, min); - } +```Go +package main + +import ( + "math" +) + +// 1、 判断两个字符串是否互为变形词 +func isDeformation(str1, str2 string) bool { + if len(str1) == 0 || len(str2) == 0 || len(str1) != len(str2) { + return false + } + + chars1 := []byte(str1) + chars2 := []byte(str2) + + // 字符词频统计表 + m := make([]int, 256) + + // 对第一个字符串中的字符进行词频统计 + for _, c := range chars1 { + m[c]++ + } + + // 用第二个字符串的字符去消除词频 + for _, c := range chars2 { + if m[c] == 0 { + return false + } + m[c]-- + } + + return true +} + +// 2、 移除字符串中连续出现k个0的子串 +func removeKZeros(str string, k int) string { + if len(str) == 0 || k < 1 { + return str + } + + chars := []byte(str) + count := 0 + start := -1 + + for i := 0; i < len(chars); i++ { + if chars[i] == '0' { + count++ + if start == -1 { + start = i + } + } else { + // 如果不等于'0'需要从start位置开始,去掉count个'0'字符 + if count == k { + for ; count != 0; count++ { + // ascii码空白字符的表示为十进制的0。chars[1] = 0 表示把1位置的字符,替换为空白符 + chars[start] = 0 + start++ + } + } + // 一轮剔除结束,count和start归位 + count = 0 + start = -1 + } + } + + // 最后一轮,即如果字符串是以'0'字符结尾的。最后要单独结算一次 + if count == k { + for ; count != 0; count-- { + chars[start] = 0 + start++ + } + } + + return string(chars) +} + +// 3、判断字符数组中,是否所有的字符均出现一次 +func isUnique(chars []byte) bool { + if len(chars) == 0 { + return true + } + + m := make([]bool, 256) + for i := 0; i < len(chars); i++ { + if m[chars[i]] { + return false + } + m[chars[i]] = true + } + + return true +} +// 4、括号字符匹配问题:输入一个字符串,包含'(','[','{',')',']','}'几种括号,求是否是括号匹配的结果。 +func isValid(s string) bool { + if len(s) == 0 { + return true + } + + chars := []byte(s) + stack := make([]byte, 0) + + for i := 0; i < len(chars); i++ { + c := chars[i] + // 遇到左括号,添加相应的右括号 + if c == '(' || c == '[' || c == '{' { + if c == '(' { + stack = append(stack, ')') + } + if c == '[' { + stack = append(stack, ']') + } + if c == '{' { + stack = append(stack, '}') + } + } else { // 遇到右括号,弹出栈,比对相等 + if len(stack) == 0 { + return false + } + + last := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if c != last { + return false + } + } + } + + // 遍历结束,栈刚好为空。满足匹配要求 + return len(stack) == 0 +} + +// 5、求一个字符串无重复最长子串 +// 子串和子序列的区别,子串必须要连续,子序列不一定要连续。 +// 遇到子串和子序列的问题,可以按照一种经典思路: +// 按照i位置结尾的情况下答案是什么?求所有可能的结尾即可,所有位置结尾的答案都求出,最大的就是我们的目标答案 +// 时间复杂度O(N),空间复杂度O(1),由于申请的空间是固定长度256 +func lengthOfLongestSubstring(s string) int { + // base case 过滤无效参数 + if len(s) == 0 { + return 0 + } + + chars := []byte(s) + m := make([]int, 256) + // 辅助数组。保存字符出现的位置,字符的范围为可显示字符0~127,扩展ascii字符128~255。0~255共256 + for i := 0; i < 256; i++ { + // 默认所有的字符都没出现过 + m[i] = -1 + } + + // i位置往左推,推不动的位置第一个因素是再次遇到了i位置上的元素,第二个因素是i-1位置当初推了多远。 + // 这两个因素的限制,哪个限制位置离当前i位置近,就是当前字符i最远推到的位置,map[i] + // 收集答案。len是收集全局的最大长度 + length := 0 + pre := -1 // i-1位置结尾的情况下,往左推,推不动的位置是谁。用来每次保存i之前一个位置的答案 + cur := 0 + + for i := 0; i != len(chars); i++ { + // i位置结尾的情况下,往左推,推不动的位置是谁 + // pre (i-1信息) 更新成 pre(i 结尾信息) + // 上次推不动的,和当前字符上次出现的位置map[str[i]]的位置,取大的 + pre = int(math.Max(float64(pre), float64(m[chars[i]]))) + // 找到了当前推不动的位置,当前不重复子串的长度就是i-pre + cur = i - pre + // 全局最大的子串长度,是否被更新,决定是否要收集 + length = int(math.Max(float64(length), float64(cur))) + // 更新当前字符出现的位置是当前位置 + m[chars[i]] = i + } + + return length +} + +// 6、最长回文子串问题。 +// 该解法是扩散法。时间复杂度为O(N * N)。(最优解是马拉车算法,可以优化该题到O(N),不掌握) +func longestPalindrome2(s string) string { + if len(s) == 0 { + return s + } + + // 全局最大回文长度 + res := 1 + // 全局最大回文长度对应的左位置 + ll := 0 + // 全局最大回文长度对应的右位置 + rr := 0 + + for i := 0; i < len(s); i++ { + // 以i为下标的奇数情况,是否有更大的len来更新res + l := i - 1 + r := i + 1 + // l和r都在合法范围。且l和r位置字符相等,可以继续扩散 + for l >= 0 && r < len(s) && s[l] == s[r] { + length := r - l + 1 + // 更新最长回文串的长度 + if length > res { + res = length + ll = l + rr = r + } + // 扩散 + l-- + r++ + } + + // 以i为下标偶数的情况。是否有更大的len来更新全局res + l = i + r = i + 1 + // l和r都在合法范围。且l和r位置字符相等,可以继续扩散 + for l >= 0 && r < len(s) && s[l] == s[r] { + length := r - l + 1 + // 更新最长回文串的长度 + if length > res { + res = length + ll = l + rr = r + } + // 扩散 + l-- + r++ + } + } + return s[ll : rr+1] // 等价于s.subString(2, 7)都是左闭右开 +} +// 7、字符串最长公共前缀问题 +func longestCommonPrefix(strs []string) string { + if len(strs) == 0 { + return "" + } + + // 拿出第一个字符串。当成初始值 + chars := []byte(strs[0]) + // 所有字符串都匹配的最大长度,等同于每个字符串和初始字符串匹配的全局最小长度 + min := math.MaxInt + + for _, str := range strs { + tmp := []byte(str) + index := 0 + + for index < len(tmp) && index < len(chars) { + if chars[index] != tmp[index] { + break + } + index++ + } + + // 更新min + min = int(math.Min(float64(index), float64(min))) + // 如果有任意一个字符串和初始串不匹配,直接返回"" + if min == 0 { + return "" + } + } + return strs[0][0:min] // strs[0].substring(0, min); } ``` \ No newline at end of file From 6a90a5aefc9a2de8acc4d94bdbf091e961ae3e09 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Tue, 5 Apr 2022 21:24:45 +0800 Subject: [PATCH 15/18] dp interview --- ...50\346\200\201\350\247\204\345\210\222.md" | 565 +++++++++--------- 1 file changed, 275 insertions(+), 290 deletions(-) diff --git "a/28-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\212\250\346\200\201\350\247\204\345\210\222.md" "b/28-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\212\250\346\200\201\350\247\204\345\210\222.md" index ecc3c2b..da8bf17 100644 --- "a/28-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\212\250\346\200\201\350\247\204\345\210\222.md" +++ "b/28-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\212\250\346\200\201\350\247\204\345\210\222.md" @@ -1,293 +1,278 @@ -```java -package com.xiaodai.algorithm; - -/** - * Author :dai - * Date :2021/3/31 11:54 上午 - * Description:动态规划专题整理 - */ -public class DPExampleUtil { - - /** - * 1、🎒背包问题:给定两个长度都为N的数组weights和values,weight[i]和values[i]分别代表i号物品的重量和价值。 - * 给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少? - * - * @param w 重量数组 - * @param v 价值数组 - * @param bag 背包的最大容量 - * @return 返回该背包所能装下的最大价值 - */ - public static int getMaxValue(int[] w, int[] v, int bag) { - // 初始传入w,v。index位置开始,alreadyW表示在index位置的时候,重量已经到达了多少 - return process(w, v, 0, 0, bag); - } - - // 暴力递归的第一种尝试 - // 0..index-1上做了货物的选择,使得你已经达到的重量是多少 alreadyW - // 如果返回-1,认为没有方案 - // 如果不返回-1,认为返回的值是真实价值 - public static int process(int[] w, int[] v, int index, int alreadyW, int bag) { - // base case - if (alreadyW > bag) { - return -1; - } - // 重量没超 - if (index == w.length) { - return 0; - } - // 当前不选择index的货物情况下,后续的价值 - // 无需传递当前index的重量,且p1就是总价值 - int p1 = process(w, v, index + 1, alreadyW, bag); - // 当前选择了index的货物,把重量加上,继续向下递归 - int p2next = process(w, v, index + 1, alreadyW + w[index], bag); - // p2表示要了当前货物之后总价值应该是后续价值加上当前价值 - int p2 = -1; - if (p2next != -1) { - p2 = v[index] + p2next; - } - return Math.max(p1, p2); - - } - - - /** - * 背包问题的第二种暴力尝试。 - * - * @param w 重量数组 - * @param v 价值数组 - * @param bag 背包容量 - * @return 返回给定背包容量所能装下的最大价值 - */ - public static int maxValue(int[] w, int[] v, int bag) { - // 相比上一个暴力递归尝试,去掉了alreadyW。用背包剩余空间代替;rest表示背包剩余空间,初始剩余空间就是背包容量 - return process(w, v, 0, bag); - } - - public static int process(int[] w, int[] v, int index, int rest) { - // base case 1 无效方案。背包剩余容量装不下当前重量的情况 - if (rest < 0) { - return -1; - } - // rest >=0。index来到终止位置,没货物了,当前返回0价值 - // base case 2 - if (index == w.length) { - return 0; - } - // 有货也有空间。当前index不选择,得到p1总价值 - int p1 = process(w, v, index + 1, rest); - int p2 = -1; - // 选择了index位置,剩余空间减去当前重量 - int p2Next = process(w, v, index + 1, rest - w[index]); - // 选择index的总价值,是index...的价值加上个当前index的价值 - if (p2Next != -1) { - p2 = v[index] + p2Next; - } - return Math.max(p1, p2); - } - - - /** - * 0-1背包问题:动态规划解决方案。在暴力递归的思路上改进 - *

- * 以背包问题举例,我们每一个重量有要和不要两个选择,且都要递归展开。那么我们的递归时间复杂度尾O(2^N)。 - * 而记忆化搜索,根据可变参数得到的长为N价值为W的二维表,那么我们的时间复杂度为O(N*bag)。 - * 如果递归过程中状态转移有化简继续优化的可能,例如枚举。那么经典动态规划可以继续优化, - * 否则记忆化搜索和动态规划的时间复杂度是一样的 - * - * @param w 重量数组 - * @param v 价值数组 - * @param bag 背包容量 - * @return 返回价值 - */ - public static int dpWay(int[] w, int[] v, int bag) { - int N = w.length; - // 准备一张dp表,行号为我们的重量范围bag+1。列为我们的价值数目个数的范围N+1。dp数组装下所有的可能性。 - int[][] dp = new int[N + 1][bag + 1]; - // 由于暴力递归中index==w.length的时候,总是返回0。所以: - // dp[N][...] = 0。整形数组初始化为0,无需处理 - // 由于N行已经初始化为0,我们从N-1开始。填我们的dp表 - for (int index = N - 1; index >= 0; index--) { - // 剩余空间从0开始,一直填写到bag - for (int rest = 0; rest <= bag; rest++) { // rest < 0 - // 通过正常位置的递归处理。我们转而填写我们的dp表 - // 所以我们p1等于dp表的下一层向上一层返回 - int p1 = dp[index + 1][rest]; - int p2 = -1; - // rest - w[index] 不越界 - if (rest - w[index] >= 0) { - p2 = v[index] + dp[index + 1][rest - w[index]]; - } - // p1和p2取最大值 - dp[index][rest] = Math.max(p1, p2); - } - } - // 最终返回dp表的0,bag位置,就是我们暴力递归的主函数调用 - return dp[0][bag]; - } - - - /** - * 2、最长递增子序列问题 - * 问题描述:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 - * 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 - * - * 例如:nums = [10,9,2,5,3,7,101,18], 返回结果是4。最长递增子序列是 [2,3,7,101],因此长度为 4 。 - **/ - public int lengthOfLIS(int[] nums) { - if (nums.length == 0) { - return 0; - } - int[] dp = new int[nums.length]; - dp[0] = 1; - // 全局最大 - int max = 1; - for (int i = 1; i < nums.length; i++) { - // 默认每个元素的dp[i]都为1,表示自己形成的递增子序列 - dp[i] = 1; - - - for (int j = 0; j < i; j++) { - // 如果在当前位置的前面,存在一个比自己小的元素,该元素的dp[j]加上当前元素形成的新的dp[j] + 1比dp[i]大。更新这个dp[i]。否则不更新 - if (nums[i] > nums[j]) { - dp[i] = Math.max(dp[i], dp[j] + 1); - } - } - // 最上层循环,每一轮检查是否需要更新全局max - max = Math.max(max, dp[i]); - } - return max; - } - - /** - * 3、最大连续子数组的和(最大子序和) - * - * 问题描述:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 - * 例如:nums = [-2,1,-3,4,-1,2,1,-5,4],返回6。连续子数组 [4,-1,2,1] 的和最大,为 6 。 - * - **/ - public int maxSubArray(int[] nums) { - if(nums == null || nums.length == 0) { - return 0; - } - - int N = nums.length; - // dp[i] 含义:子数组必须以i结尾的时候,所有可以得到的子数组中,最大累加和是多少? - int[] dp = new int[N]; - dp[0] = nums[0]; - // 记录全局最大的子数组的和 - int max = dp[0]; - for (int i = 1; i < N; i++) { - // 当前的值 - int p1 = nums[i]; - // 当前的值和上一个位置的最大和累加 - int p2 = nums[i] + dp[i - 1]; - // dp[i]等于,当前的值,和当前值与上一个位置最大和的累加,取大的 - dp[i] = Math.max(p1, p2); - // 判断是否要更新全局最大值 - max = Math.max(max, dp[i]); - } - // 返回全局最大值 - return max; - } - - - /** - * 4、打家劫舍问题 - * - * 问题描述:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 - * 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 - * - * 示例输入:[1,2,3,1], 输出4;偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。 - * - **/ - public int rob(int[] nums) { - - if(nums == null || nums.length == 0) { - return 0; - } - int[] dp = new int[nums.length]; - - for(int i = 0; i < nums.length; i++) { - if(i == 0) { - dp[0] = nums[i]; - } - if(i == 1) { - dp[1] = Math.max(dp[0], nums[i]); - } - if(i > 1) { - dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); - } - } - return dp[nums.length - 1]; - } - - /** - * 5、爬楼梯问题。 - * - * 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 - * 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? - * - **/ - public int climbStairs(int n) { - if(n == 0) { - return 0; - } - if(n == 1) { - return 1; - } - if(n == 2) { - return 2; - } - - int[] dp = new int[n + 1]; - dp[1] = 1; - dp[2] = 2; - for(int i = 3; i <= n ; i++) { - dp[i] = dp[i-1] + dp[i-2]; - } - return dp[n]; - } - - - /** - * 6、两个字符串的最长公共子序列问题 - * - * 例如“ab1cd2ef345gh”和“opq123rs4tx5yz”的最长公共子序列为“12345”。即在两个字符串所有相等的子序列里最长的。所以返回子序列的长度5 - * - **/ - public static int lcse(char[] str1, char[] str2) { - - int[][] dp = new int[str1.length][str2.length]; - - dp[0][0] = str1[0] == str2[0] ? 1 : 0; - - - // 填第0列的所有值 - // 一旦st1r的i位置某个字符等于str2的0位置,那么之后都是1 - for (int i = 1; i < str1.length; i++) { - dp[i][0] = Math.max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0); - } - // 填第0行的所有值 - // 一旦str2的j位置某个字符等于str1的0位置,那么之后都是1 - for (int j = 1; j < str2.length; j++) { - dp[0][j] = Math.max(dp[0][j - 1], str1[0] == str2[j] ? 1 : 0); - } - - for (int i = 1; i < str1.length; i++) { - for (int j = 1; j < str2.length; j++) { - - // dp[i - 1][j]表示可能性2 - // dp[i][j - 1] 表示可能性3 - dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); - if (str1[i] == str2[j]) { - dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1); - } - } - } - return dp[str1.length - 1][str2.length - 1]; - } - - +```Go +package main + +import "math" + +// 1-1、🎒背包问题:给定两个长度都为N的数组weights和values,weight[i]和values[i]分别代表i号物品的重量和价值。 +// 给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少? +// w 重量数组 +// v 价值数组 +// bag 背包的最大容量 +// 返回该背包所能装下的最大价值 +func getMaxValue(w []int, v []int, bag int) int { + // 初始传入w,v。index位置开始,alreadyW表示在index位置的时候,重量已经到达了多少 + return processBag(w, v, 0, 0, bag) +} + +// 递归的第一种尝试 +// 0..index-1上做了货物的选择,使得你已经达到的重量是多少 alreadyW +// 如果返回-1,认为没有方案 +// 如果不返回-1,认为返回的值是真实价值 +func processBag(w []int, v []int, index int, alreadyW int, bag int) int { + // base case + if alreadyW > bag { + return -1 + } + + // 重量没超 + if index == len(w) { + return 0 + } + + // 当前不选择index的货物情况下,后续的价值 + // 无需传递当前index的重量,且p1就是总价值 + p1 := processBag(w, v, index+1, alreadyW, bag) + // 当前选择了index的货物,把重量加上,继续向下递归 + p2next := processBag(w, v, index+1, alreadyW+w[index], bag) + // p2表示要了当前货物之后总价值应该是后续价值加上当前价值 + p2 := -1 + if p2next != -1 { + p2 = v[index] + p2next + } + + return int(math.Max(float64(p1), float64(p2))) +} + +// 1-2、背包问题的第二种递归解法。 +func maxValue(w []int, v []int, bag int) int { + // 相比上一个暴力递归尝试,去掉了alreadyW。用背包剩余空间代替;rest表示背包剩余空间,初始剩余空间就是背包容量 + return processBag2(w, v, 0, bag) +} + +func processBag2(w []int, v []int, index int, rest int) int { + // base case 1 无效方案。背包剩余容量装不下当前重量的情况 + if rest < 0 { + return -1 + } + + // rest >=0。index来到终止位置,没货物了,当前返回0价值 + // base case 2 + if index == len(w) { + return 0 + } + + // 有货也有空间。当前index不选择,得到p1总价值 + p1 := processBag2(w, v, index+1, rest) + p2 := -1 + // 选择了index位置,剩余空间减去当前重量 + p2Next := processBag2(w, v, index+1, rest-w[index]) + // 选择index的总价值,是index...的价值加上个当前index的价值 + if p2Next != -1 { + p2 = v[index] + p2Next + } + + return int(math.Max(float64(p1), float64(p2))) +} + +// 1-3、0-1背包问题:动态规划解决方案。在递归的思路上改进 +// 以背包问题举例,我们每一个重量有要和不要两个选择,且都要递归展开。那么我们的递归时间复杂度尾O(2^N)。 +// 而记忆化搜索,根据可变参数得到的长为N价值为W的二维表,那么我们的时间复杂度为O(N*bag)。 +// 如果递归过程中状态转移有化简继续优化的可能,例如枚举。那么经典动态规划可以继续优化, +// 否则记忆化搜索和动态规划的时间复杂度是一样的 +func dpWay(w []int, v []int, bag int) int { + N := len(w) + // 准备一张dp表,行号为我们的重量范围bag+1。列为我们的价值数目个数的范围N+1。dp数组装下所有的可能性。 + dp := make([][]int, N+1) + for i := 0; i < N+1; i++ { + dp[i] = make([]int, bag+1) + } + + // 由于暴力递归中index==w.length的时候,总是返回0。所以: + // dp[N][...] = 0。整形数组初始化为0,无需处理 + // 由于N行已经初始化为0,我们从N-1开始。填我们的dp表 + for index := N - 1; index >= 0; index-- { + // 剩余空间从0开始,一直填写到bag + for rest := 0; rest <= bag; rest++ { + // 通过正常位置的递归处理。我们转而填写我们的dp表 + // 所以我们p1等于dp表的下一层向上一层返回 + p1 := dp[index+1][rest] + p2 := -1 + // rest - w[index] 不越界 + if rest-w[index] >= 0 { + p2 = v[index] + dp[index+1][rest-w[index]] + } + // p1和p2取最大值 + dp[index][rest] = int(math.Max(float64(p1), float64(p2))) + } + } + // 最终返回dp表的0,bag位置,就是我们暴力递归的主函数调用 + return dp[0][bag] +} + +// 2、最长递增子序列问题 +// 问题描述:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 +// 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 +// 例如:nums = [10,9,2,5,3,7,101,18], 返回结果是4。最长递增子序列是 [2,3,7,101],因此长度为 4 。 +func lengthOfLIS(nums []int) int { + if len(nums) == 0 { + return 0 + } + + dp := make([]int, len(nums)) + + dp[0] = 1 + // 全局最大 + max := 1 + + for i := 1; i < len(nums); i++ { + // 默认每个元素的dp[i]都为1,表示自己形成的递增子序列 + dp[i] = 1 + + for j := 0; j < i; j++ { + // 如果在当前位置的前面,存在一个比自己小的元素,该元素的dp[j]加上当前元素形成的新的dp[j] + 1比dp[i]大。更新这个dp[i]。否则不更新 + if nums[i] > nums[j] { + dp[i] = int(math.Max(float64(dp[i]), float64(dp[j]+1))) + } + } + + // 最上层循环,每一轮检查是否需要更新全局max + max = int(math.Max(float64(max), float64(dp[i]))) + } + return max +} + +// 3、最大连续子数组的和(最大子序和) +// 问题描述:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 +// 例如:nums = [-2,1,-3,4,-1,2,1,-5,4],返回6。连续子数组 [4,-1,2,1] 的和最大,为 6 。 +func maxSubArray(nums []int) int { + if len(nums) == 0 { + return 0 + } + + N := len(nums) + // dp[i] 含义:子数组必须以i结尾的时候,所有可以得到的子数组中,最大累加和是多少? + dp := make([]int, N) + dp[0] = nums[0] + // 记录全局最大的子数组的和 + max := dp[0] + for i := 1; i < N; i++ { + // 当前的值 + p1 := nums[i] + // 当前的值和上一个位置的最大和累加 + p2 := nums[i] + dp[i - 1] + // dp[i]等于,当前的值,和当前值与上一个位置最大和的累加,取大的 + dp[i] = int(math.Max(float64(p1), float64(p2))) + // 判断是否要更新全局最大值 + max = int(math.Max(float64(max), float64(dp[i]))) + } + // 返回全局最大值 + return max +} + +// 4、打家劫舍问题 +// 问题描述:你是一个专业的小偷,计划偷窃沿街的房屋。 +// 每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 +// 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 +// 示例输入:[1,2,3,1], 输出4;偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。 +func rob(nums []int) int { + if len(nums) == 0 { + return 0 + } + + dp := make([]int, len(nums)) + + for i := 0; i < len(nums); i++ { + if i == 0 { + dp[0] = nums[i] + } + if i == 1 { + dp[1] = int(math.Max(float64(dp[0]), float64(nums[i]))) + } + if i > 1 { + dp[i] = int(math.Max(float64(dp[i - 1]), float64(dp[i - 2] + nums[i]))) + } + } + return dp[len(nums) - 1] +} + +// 5、爬楼梯问题。 +// 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 +// 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? +func climbStairs(n int) int { + if n == 0 { + return 0 + } + if n == 1 { + return 1 + } + if n == 2 { + return 2 + } + + dp := make([]int, n + 1) + dp[1] = 1 + dp[2] = 2 + for i := 3; i <= n; i++ { + dp[i] = dp[i - 1] + dp[i - 2] + } + return dp[n] +} + +// 6、两个字符串的最长公共子序列问题 +// 例如“ab1cd2ef345gh”和“opq123rs4tx5yz”的最长公共子序列为“12345”。即在两个字符串所有相等的子序列里最长的。所以返回子序列的长度5 +func lcse(str1 []byte, str2 []byte) int { + if len(str1) == 0 { + return 0 + } + if len(str2) == 0 { + return 0 + } + + dp := make([][]int, len(str1)) + for i := 0; i < len(str1); i++ { + dp[i] = make([]int, len(str2)) + } + if str1[0] == str2[0] { + dp[0][0] = 1 + } else { + dp[0][0] = 0 + } + + // 填第0列的所有值 + // 一旦st1r的i位置某个字符等于str2的0位置,那么之后都是1 + for i := 1; i < len(str1); i++ { + flag := -1 + if str1[i] == str2[0] { + flag = 1 + } else { + flag = 0 + } + dp[i][0] = int(math.Max(float64(dp[i - 1][0]), float64(flag))) + } + + // 填第0行的所有值 + // 一旦str2的j位置某个字符等于str1的0位置,那么之后都是1 + for j := 1; j < len(str2); j++ { + flag := -1 + if str1[0] == str2[j] { + flag = 1 + } else { + flag = 0 + } + dp[0][j] = int(math.Max(float64(dp[0][j - 1]), float64(flag))) + } + + for i := 1; i < len(str1); i++ { + for j := 1; j < len(str2); j++ { + // dp[i - 1][j]表示可能性2 + // dp[i][j - 1] 表示可能性3 + dp[i][j] = int(math.Max(float64(dp[i - 1][j]), float64(dp[i][j - 1]))) + if str1[i] == str2[j] { + dp[i][j] = int(math.Max(float64(dp[i][j]), float64(dp[i - 1][j - 1] + 1))) + } + } + } + return dp[len(str1) - 1][len(str2) - 1] } ``` From 1840c2d564301f38a4bc72bcdffbfc247ab71e84 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Tue, 5 Apr 2022 22:09:58 +0800 Subject: [PATCH 16/18] refactor --- ...50\241\250\344\270\223\351\242\230\346\261\207\346\200\273.md" | 0 ...46\240\221\344\270\223\351\242\230\346\261\207\346\200\273.md" | 0 ...44\270\262\344\270\223\351\242\230\346\261\207\346\200\273.md" | 0 ...45\210\222\344\270\223\351\242\230\346\261\207\346\200\273.md" | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename "25-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\351\223\276\350\241\250.md" => "25-\351\231\204\357\274\232\351\223\276\350\241\250\344\270\223\351\242\230\346\261\207\346\200\273.md" (100%) rename "26-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\344\272\214\345\217\211\346\240\221.md" => "26-\351\231\204\357\274\232\344\272\214\345\217\211\346\240\221\344\270\223\351\242\230\346\261\207\346\200\273.md" (100%) rename "27-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\255\227\347\254\246\344\270\262.md" => "27-\351\231\204\357\274\232\345\255\227\347\254\246\344\270\262\344\270\223\351\242\230\346\261\207\346\200\273.md" (100%) rename "28-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\212\250\346\200\201\350\247\204\345\210\222.md" => "28-\351\231\204\357\274\232\345\212\250\346\200\201\350\247\204\345\210\222\344\270\223\351\242\230\346\261\207\346\200\273.md" (100%) diff --git "a/25-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\351\223\276\350\241\250.md" "b/25-\351\231\204\357\274\232\351\223\276\350\241\250\344\270\223\351\242\230\346\261\207\346\200\273.md" similarity index 100% rename from "25-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\351\223\276\350\241\250.md" rename to "25-\351\231\204\357\274\232\351\223\276\350\241\250\344\270\223\351\242\230\346\261\207\346\200\273.md" diff --git "a/26-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\344\272\214\345\217\211\346\240\221.md" "b/26-\351\231\204\357\274\232\344\272\214\345\217\211\346\240\221\344\270\223\351\242\230\346\261\207\346\200\273.md" similarity index 100% rename from "26-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\344\272\214\345\217\211\346\240\221.md" rename to "26-\351\231\204\357\274\232\344\272\214\345\217\211\346\240\221\344\270\223\351\242\230\346\261\207\346\200\273.md" diff --git "a/27-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\255\227\347\254\246\344\270\262.md" "b/27-\351\231\204\357\274\232\345\255\227\347\254\246\344\270\262\344\270\223\351\242\230\346\261\207\346\200\273.md" similarity index 100% rename from "27-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\255\227\347\254\246\344\270\262.md" rename to "27-\351\231\204\357\274\232\345\255\227\347\254\246\344\270\262\344\270\223\351\242\230\346\261\207\346\200\273.md" diff --git "a/28-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\212\250\346\200\201\350\247\204\345\210\222.md" "b/28-\351\231\204\357\274\232\345\212\250\346\200\201\350\247\204\345\210\222\344\270\223\351\242\230\346\261\207\346\200\273.md" similarity index 100% rename from "28-\347\256\227\346\263\225\351\235\242\350\257\225\344\270\223\351\242\230-\345\212\250\346\200\201\350\247\204\345\210\222.md" rename to "28-\351\231\204\357\274\232\345\212\250\346\200\201\350\247\204\345\210\222\344\270\223\351\242\230\346\261\207\346\200\273.md" From 21d6ce7d2e53d31045143fe180718c98b58b8986 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Tue, 19 Apr 2022 14:13:12 +0800 Subject: [PATCH 17/18] fix readme --- README.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 9057af1..ff80dde 100644 --- a/README.md +++ b/README.md @@ -8,33 +8,33 @@ ## 笔记阅读传送门 -- GitHub Pages 完整阅读:[进入](https://dairongpeng.github.io/algorithm-note/) +- 完整阅读:[进入](https://www.yuque.com/dairongpeng/no7xzv/zw88wn) ## 目录概览 -- [x] [第一节 复杂度、排序、二分、异或](https://dairongpeng.github.io/algorithm-note/01) -- [x] [第二节 链表、栈、队列、递归、哈希表、顺序表](https://dairongpeng.github.io/algorithm-note/02) -- [x] [第三节 归并排序、随机快排介绍](https://dairongpeng.github.io/algorithm-note/03) -- [x] [第四节 比较器与堆](https://dairongpeng.github.io/algorithm-note/04) -- [x] [第五节 前缀树、桶排序以及排序总结](https://dairongpeng.github.io/algorithm-note/05) -- [x] [第六节 链表相关面试题总结](https://dairongpeng.github.io/algorithm-note/06) -- [x] [第七节 二叉树基本算法](https://dairongpeng.github.io/algorithm-note/07) -- [x] [第八节 二叉树的递归思维建立](https://dairongpeng.github.io/algorithm-note/08) -- [x] [第九节 认识贪心算法](https://dairongpeng.github.io/algorithm-note/09) -- [x] [第十节 并查集、图相关算法介绍](https://dairongpeng.github.io/algorithm-note/10) -- [x] [第十一节 暴力递归思维、动态规划思维建立](https://dairongpeng.github.io/algorithm-note/11) -- [x] [第十二节 用简单暴力递归思维推导动态规划思维](https://dairongpeng.github.io/algorithm-note/12) -- [x] [第十三节 单调栈和窗口及其更新结构](https://dairongpeng.github.io/algorithm-note/13) -- [x] [第十四节 类似斐波那契数列的递归](https://dairongpeng.github.io/algorithm-note/14) -- [x] [第十五节 认识KMP算法与bfprt算法](https://dairongpeng.github.io/algorithm-note/15) -- [x] [第十六节 认识Manacher(马拉车)算法](https://dairongpeng.github.io/algorithm-note/16) -- [x] [第十七节 认识Morris遍历](https://dairongpeng.github.io/algorithm-note/17) -- [x] [第十八节 线段树](https://dairongpeng.github.io/algorithm-note/18) -- [x] [第十九节 打表技巧和矩阵处理技巧](https://dairongpeng.github.io/algorithm-note/19) -- [x] [第二十节 组累加和问题整理](https://dairongpeng.github.io/algorithm-note/20) -- [x] [第二十一节 哈希函数有关的结构和岛问题](https://dairongpeng.github.io/algorithm-note/21) -- [x] [第二十二节 解决资源限制类题目](https://dairongpeng.github.io/algorithm-note/22) -- [x] [第二十三节 有序表原理及扩展](https://dairongpeng.github.io/algorithm-note/23) -- [x] [第二十四节 AC自动机和卡特兰数](https://dairongpeng.github.io/algorithm-note/23) +- [x] [第一节 复杂度、排序、二分、异或](https://www.yuque.com/dairongpeng/no7xzv/tkqyqh) +- [x] [第二节 链表、栈、队列、递归、哈希表、顺序表](https://www.yuque.com/dairongpeng/no7xzv/wxk6gu) +- [x] [第三节 归并排序、随机快排介绍](https://www.yuque.com/dairongpeng/no7xzv/wck819) +- [x] [第四节 堆、结构体排序](https://www.yuque.com/dairongpeng/no7xzv/wck819) +- [x] [第五节 前缀树、桶排序以及排序总结](https://www.yuque.com/dairongpeng/no7xzv/mkuhxb) +- [x] [第六节 链表相关高频题总结](https://www.yuque.com/dairongpeng/no7xzv/zk422u) +- [x] [第七节 二叉树基本算法](https://www.yuque.com/dairongpeng/no7xzv/os4mpm) +- [x] [第八节 二叉树的递归解题思路](https://www.yuque.com/dairongpeng/no7xzv/bvkf4t) +- [x] [第九节 贪心算法解题思路](https://www.yuque.com/dairongpeng/no7xzv/runxe4) +- [x] [第十节 并查集、图相关算法介绍](https://www.yuque.com/dairongpeng/no7xzv/fssemq) +- [x] [第十一节 暴力递归、动态规划](https://www.yuque.com/dairongpeng/no7xzv/sa6xlq) +- [x] [第十二节 简单暴力递归推导动态规划思路](https://www.yuque.com/dairongpeng/no7xzv/pbvuat) +- [x] [第十三节 单调栈和窗口结构](https://www.yuque.com/dairongpeng/no7xzv/xwqq1z) +- [x] [第十四节 类似斐波那契数列的递归](https://www.yuque.com/dairongpeng/no7xzv/nw8vti) +- [x] [第十五节 KMP算法与BfPrt算法总结](https://www.yuque.com/dairongpeng/no7xzv/pkwrz3) +- [x] [第十六节 Manacher(马拉车)算法介绍](https://www.yuque.com/dairongpeng/no7xzv/icb5d0) +- [x] [第十七节 认识Morris遍历](https://www.yuque.com/dairongpeng/no7xzv/amf408) +- [x] [第十八节 线段树(interval-tree)](https://www.yuque.com/dairongpeng/no7xzv/oa8zft) +- [x] [第十九节 打表技巧和矩阵处理法](https://www.yuque.com/dairongpeng/no7xzv/fspk7r) +- [x] [第二十节 组累加和问题整理](https://www.yuque.com/dairongpeng/no7xzv/mz72mg) +- [x] [第二十一节 哈希、位图、布隆过滤器及岛问题](https://www.yuque.com/dairongpeng/no7xzv/uhrorf) +- [x] [第二十二节 资源限制类问题总结](https://www.yuque.com/dairongpeng/no7xzv/ks9lg4) +- [x] [第二十三节 有序表介绍及其原理](https://www.yuque.com/dairongpeng/no7xzv/ks0v3y) +- [x] [第二十四节 AC自动机](https://www.yuque.com/dairongpeng/no7xzv/ah28p1) From 630546b48f7a492b6ae4afa04a8199382fd6b5d0 Mon Sep 17 00:00:00 2001 From: dairongpeng Date: Wed, 7 Sep 2022 21:02:24 +0800 Subject: [PATCH 18/18] fix: .gitignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 088b537..1d255af 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,13 @@ # Log file *.log +# Files generated by JetBrains IDEs, e.g. IntelliJ IDEA +.idea/ +*.iml + +# Vscode files +.vscode + # BlueJ files *.ctxt