diff --git a/.gitattributes b/.gitattributes index 5d4be31..7edf504 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -*.md linguist-language=Java +*.md linguist-language=Go 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 diff --git "a/01-\345\244\215\346\235\202\345\272\246\343\200\201\346\216\222\345\272\217\343\200\201\344\272\214\345\210\206\343\200\201\345\274\202\346\210\226.md" "b/01-\345\244\215\346\235\202\345\272\246\343\200\201\346\216\222\345\272\217\343\200\201\344\272\214\345\210\206\343\200\201\345\274\202\346\210\226.md" index 48415b2..418b8bf 100644 --- "a/01-\345\244\215\346\235\202\345\272\246\343\200\201\346\216\222\345\272\217\343\200\201\344\272\214\345\210\206\343\200\201\345\274\202\346\210\226.md" +++ "b/01-\345\244\215\346\235\202\345\272\246\343\200\201\346\216\222\345\272\217\343\200\201\344\272\214\345\210\206\343\200\201\345\274\202\346\210\226.md" @@ -2,10 +2,10 @@ # 1 时间复杂度、空间复杂度、排序、异或运算 ## 1.1 时间复杂度 - 常数时间操作: -1. 算数运算:+ - * / -2. 位运算:>>(带符号右移动)、 >>>(不带符号右移动) 、 <<、 | 、& 、^ +1. 算数运算:`+` `-` `*` `/` +2. 位运算:`>>`(带符号右移动)、 `>>>`(不带符号右移动) 、 `<<`、 `|` 、`&` 、`^` -==带符号就是最高位补符号位,不带符号就是最高位补0== +> 带符号就是最高位补符号位,不带符号就是最高位补0 3. 赋值操作:比较,自增,自减操作 4. 数组寻址等 @@ -20,361 +20,89 @@ y = an^2 + bn + c ``` -==忽略掉低阶项,忽略掉常数项,忽略掉高阶项的系数,得到时间复杂度为n^2== +> 忽略掉低阶项,忽略掉常数项,忽略掉高阶项的系数,得到时间复杂度为n^2 ### 1.1.1 排序操作 - +--- #### 1.1.1.1 选择排序 -```Java -package class01; - -import java.util.Arrays; - -public class Code01_SelectionSort { - - public static void selectionSort(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - // 0 ~ N-1 - // 1~n-1 - // 2 - for (int i = 0; i < arr.length - 1; i++) { // i ~ N-1 - // 最小值在哪个位置上 i~n-1 - int minIndex = i; - for (int j = i + 1; j < arr.length; j++) { // i ~ N-1 上找最小值的下标 - minIndex = arr[j] < arr[minIndex] ? j : minIndex; - } - swap(arr, i, minIndex); - } - } - - public static void swap(int[] arr, int i, int j) { - int tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } - - // for test - public static void comparator(int[] arr) { - Arrays.sort(arr); - } +```Go +package main - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - // Math.random() [0,1) - // Math.random() * N [0,N) - // (int)(Math.random() * N) [0, N-1] - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - // [-? , +?] - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - return arr; - } - - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; - } - return res; - } +import "fmt" - // for test 对数器 - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; - } +func main() { + arr := []int{1, 4, 5, 8, 3, 1, 0, 22, 53, 21} + selectionSort(arr) + fmt.Println(arr) +} - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); +// 选择排序 +func selectionSort(arr []int) { + if len(arr) == 0 || len(arr) < 2 { + return } - // for test - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 100; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - selectionSort(arr1); - comparator(arr2); - if (!isEqual(arr1, arr2)) { - succeed = false; - printArray(arr1); - printArray(arr2); - break; + // 在i到n-1的位置依次处理, i从0开始 + for i := 0; i < len(arr)-1; i++ { + minIndex := i + // 寻找当前剩余元素的最小值放在当前位置 + for j := i + 1; j < len(arr); j++ { + if arr[j] < arr[minIndex] { + minIndex = j } } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - - int[] arr = generateRandomArray(maxSize, maxValue); - printArray(arr); - selectionSort(arr); - printArray(arr); + swap(arr, i, minIndex) } +} +func swap(arr []int, a, b int) { + tmp := arr[a] + arr[a] = arr[b] + arr[b] = tmp } ``` +--- #### 1.1.1.2 冒泡排序 -```Java -package class01; - -import java.util.Arrays; - -public class Code02_BubbleSort { - - public static void bubbleSort(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - // 0 ~ N-1 - // 0 ~ N-2 - // 0 ~ N-3 - for (int e = arr.length - 1; e > 0; e--) { // 0 ~ e - for (int i = 0; i < e; i++) { - if (arr[i] > arr[i + 1]) { - swap(arr, i, i + 1); - } - } - } - } - - // 交换arr的i和j位置上的值 - public static void swap(int[] arr, int i, int j) { - arr[i] = arr[i] ^ arr[j]; - arr[j] = arr[i] ^ arr[j]; - arr[i] = arr[i] ^ arr[j]; - } - - // for test - public static void comparator(int[] arr) { - Arrays.sort(arr); - } - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - return arr; - } - - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; - } - return res; - } - - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; - } - - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - // for test - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 100; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - bubbleSort(arr1); - comparator(arr2); - if (!isEqual(arr1, arr2)) { - succeed = false; - break; +```Go +// 冒泡排序 +func bubbleSort(arr []int) { + if len(arr) == 0 || len(arr) < 2 { + return + } + + // 外循环从最大位置开始处理,形成冒泡的效果 + for i := len(arr) - 1; i > 0; i-- { + // 内循环的一轮次,搞定外循环的一个位置。 + for j := 0; j < i; j ++ { + if arr[j] > arr[i] { + swap(arr, i, j) } } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - - int[] arr = generateRandomArray(maxSize, maxValue); - printArray(arr); - bubbleSort(arr); - printArray(arr); } - } ``` +--- #### 1.1.1.3 插入排序 -```Java -package class01; - -import java.util.Arrays; - -public class Code03_InsertionSort { - - public static void insertionSort(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - // 0~0 有序的 - // 0~i 想有序 - for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序 - for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { - swap(arr, j, j + 1); +```Go +// 选择排序 +func insertionSort(arr []int) { + if len(arr) == 0 || len(arr) < 2 { + return + } + + // 类比打扑克牌 + for i := 1; i < len(arr); i++ { + // 每一轮内循环,与前面的元素来一轮比较,达到的效果是最小元素经过一轮内循环总能放到0位置 + for j := i - 1; j >=0 ; j-- { + if arr[j] > arr[j + 1] { + swap(arr, j, j + 1) } } } - - // i和j是一个位置的话,会出错 - public static void swap(int[] arr, int i, int j) { - arr[i] = arr[i] ^ arr[j]; - arr[j] = arr[i] ^ arr[j]; - arr[i] = arr[i] ^ arr[j]; - } - - // for test - public static void comparator(int[] arr) { - Arrays.sort(arr); - } - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - // Math.random() -> [0,1) 所有的小数,等概率返回一个 - // Math.random() * N -> [0,N) 所有小数,等概率返回一个 - // (int)(Math.random() * N) -> [0,N-1] 所有的整数,等概率返回一个 - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; // 长度随机 - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - - (int) (maxValue * Math.random()); - } - return arr; - } - - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; - } - return res; - } - - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; - } - - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - // for test - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 100; // 随机数组的长度0~100 - int maxValue = 100;// 值:-100~100 - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - insertionSort(arr1); - comparator(arr2); - if (!isEqual(arr1, arr2)) { - // 打印arr1 - // 打印arr2 - succeed = false; - break; - } - } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - - int[] arr = generateRandomArray(maxSize, maxValue); - printArray(arr); - insertionSort(arr); - printArray(arr); - } - } - ``` -==插入排序和前面两种排序的不同是在于,插入排序跟数组初始顺序有关,在初始有序的情况下,有可能时间复杂度为O(N),有可能为O(N ^2),但是我们估计时间复杂度要按照最差的情况来估计,所以插入排序的时间复杂度仍然O(N ^2)== +> 插入排序和前面两种排序的不同是在于,插入排序跟数组初始顺序有关,在初始有序的情况下,有可能时间复杂度为O(N),有可能为O(N ^2),但是我们估计时间复杂度要按照最差的情况来估计,所以插入排序的时间复杂度仍然O(N ^2) ## 1.2 空间复杂度 @@ -412,77 +140,28 @@ public class Code03_InsertionSort { > 二分查找值,基于有序数组,算法复杂度为二分了多少次,O(log2N)可以写成O(logN) -> 123579 - -```Java -package class01; - -import java.util.Arrays; - -public class Code04_BSExist { - - public static boolean exist(int[] sortedArr, int num) { - if (sortedArr == null || sortedArr.length == 0) { - return false; - } - int L = 0; - int R = sortedArr.length - 1; - int mid = 0; - // L..R - while (L < R) { - // mid = (L+R) / 2; - // L 10亿 R 18亿 - // mid = L + (R - L) / 2 - // N / 2 N >> 1 - mid = L + ((R - L) >> 1); // mid = (L + R) / 2 - if (sortedArr[mid] == num) { - return true; - } else if (sortedArr[mid] > num) { - R = mid - 1; - } else { - L = mid + 1; - } - } - return sortedArr[L] == num; - } - - // for test - public static boolean test(int[] sortedArr, int num) { - for(int cur : sortedArr) { - if(cur == num) { - return true; - } - } - return false; - } - - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - return arr; +```Go +// 有序数组二分查找 +func exist(arr []int, target int) bool { + if len(arr) == 0 { + return false } - - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 10; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr = generateRandomArray(maxSize, maxValue); - Arrays.sort(arr); - int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - if (test(arr, value) != exist(arr, value)) { - succeed = false; - break; - } + + var L = 0 + var R = len(arr) - 1 + var mid = 0 + for L < R { + // 防止整数越界 + mid = L + (R - L) / 2 + if arr[mid] == target { + return true + } else if arr[mid] < target { // mid位置已经比较过,L置为mid+1 + L = mid + 1 + } else if arr[mid] > target { // mid位置已经比较过,R置为mid-1 + R = mid - 1 } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); } - + return arr[L] == target } ``` @@ -490,201 +169,44 @@ public class Code04_BSExist { > 122222333578888999999 找大于等于2最左侧的位置 -```Java -package class01; - -import java.util.Arrays; - -public class Code05_BSNearLeft { - - // 在arr上,找满足>=value的最左位置 - public static int nearestIndex(int[] arr, int value) { - int L = 0; - int R = arr.length - 1; - int index = -1; // 记录最左的对号 - while (L <= R) { - int mid = L + ((R - L) >> 1); - if (arr[mid] >= value) { - index = mid; - R = mid - 1; - } else { - L = mid + 1; - } - } - return index; - } - - // for test - public static int test(int[] arr, int value) { - for (int i = 0; i < arr.length; i++) { - if (arr[i] >= value) { - return i; - } - } - return -1; - } - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - return arr; - } - - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 10; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr = generateRandomArray(maxSize, maxValue); - Arrays.sort(arr); - int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - if (test(arr, value) != nearestIndex(arr, value)) { - printArray(arr); - System.out.println(value); - System.out.println(test(arr, value)); - System.out.println(nearestIndex(arr, value)); - succeed = false; - break; - } - } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - } - -} -``` - -3. 在一个有序数组中,找<=某个数最右侧的位置 - -```Java - -package class01; - -import java.util.Arrays; - -public class Code05_BSNearRight { - - // 在arr上,找满足<=value的最右位置 - public static int nearestIndex(int[] arr, int value) { - int L = 0; - int R = arr.length - 1; - int index = -1; // 记录最右的对号 - while (L <= R) { - int mid = L + ((R - L) >> 1); - if (arr[mid] <= value) { - index = mid; - L = mid + 1; - } else { - R = mid - 1; - } - } - return index; - } - - // for test - public static int test(int[] arr, int value) { - for (int i = arr.length - 1; i >= 0; i--) { - if (arr[i] <= value) { - return i; - } - } - return -1; - } - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - return arr; - } - - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 10; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr = generateRandomArray(maxSize, maxValue); - Arrays.sort(arr); - int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - if (test(arr, value) != nearestIndex(arr, value)) { - printArray(arr); - System.out.println(value); - System.out.println(test(arr, value)); - System.out.println(nearestIndex(arr, value)); - succeed = false; - break; - } - } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - } - +```Go +// 在一个有序数组上,找到大于等于value的最左位置 +func nearestLeftIndex(arr []int, value int) int { + L := 0 + R := len(arr) - 1 + index := -1 + for L <= R { + mid := L + (R - L) / 2 + // 当前中间位置大于value,寻找大于value最左位置,缩小右边界,继续寻找 + if arr[mid] >= value { + index = mid + R = mid - 1 + } else { + L = mid + 1 + } + } + return index } - ``` -4. 局部最小值问题 - -> 无序数组,任意两个相邻的数不相等,返回一个局部最小值 +3. 在一个有序数组中,找小于等于某个数最右侧的位置 -```Java -package class01; - -public class Code06_BSAwesome { - - public static int getLessIndex(int[] arr) { - if (arr == null || arr.length == 0) { - return -1; // no exist - } - if (arr.length == 1 || arr[0] < arr[1]) { - return 0; +```Go +// 在一个有序数组上,找到小于等于value的最右位置 +func nearestRightIndex(arr []int, value int) int { + L := 0 + R := len(arr) - 1 + index := -1 + for L <= R { + mid := L + (R - L) / 2 + if arr[mid] <= value { + index = mid + L = mid + 1 + } else { + L = mid - 1 } - if (arr[arr.length - 1] < arr[arr.length - 2]) { - return arr.length - 1; - } - int left = 1; - int right = arr.length - 2; - int mid = 0; - while (left < right) { - mid = (left + right) / 2; - if (arr[mid] > arr[mid - 1]) { - right = mid - 1; - } else if (arr[mid] > arr[mid + 1]) { - left = mid + 1; - } else { - return mid; - } - } - return left; } - + return index } ``` @@ -694,167 +216,127 @@ public class Code06_BSAwesome { 同或运算:相同为1, 不同为0,不掌握 -==上述特别不容易记住,异或运算就记成无进位相加:比如十进制6异或7,就理解为110和111按位不进位相加,得到001== +>上述规则不容易记住,异或运算就记成无进位相加:比如十进制6异或7,就理解为110和111按位不进位相加,得到001 1. 所以 0^N = N , N^N = 0 2. 异或运算满足交换律和结合律,所以A异或B异或C = A异或(B异或C) = (A异或C)异或B -题目一:如何不用额外变量就交换两个数 +**题目一:如何不用额外变量就交换两个数** ```shell -a = x b = y两个数交换位置 +# 三步操作,实现交换ab的值 +# a = x b = y两个数交换位置 a = a ^ b # 第一步操作,此时 a = x^y , b=y b = a ^ b # 第二步操作,此时 a = x^y , b = x^y^y => b = x^0 => b = x a = a ^ b # 第三步操作,此时 a = x^y^x, b = x, a=> x^x^y => a=y - -三步操作,实现交换ab的值 - ``` -```Java -package class01; - -public class Test { - - public static void main(String[] args) { - int a = 6; - int b = 6; - - - a = a ^ b; - b = a ^ b; - a = a ^ b; - - - System.out.println(a); - System.out.println(b); - - - - - int[] arr = {3,1,100}; - - System.out.println(arr[0]); - System.out.println(arr[2]); - - swap(arr, 0, 0); - - System.out.println(arr[0]); - System.out.println(arr[2]); - - - - } - - - public static void swap (int[] arr, int i, int j) { - // arr[0] = arr[0] ^ arr[0]; - arr[i] = arr[i] ^ arr[j]; - arr[j] = arr[i] ^ arr[j]; - arr[i] = arr[i] ^ arr[j]; - } - - +```Go +// IsoOr 异或交换数据 +func IsoOr() { + a := 3 + b := 4 + a = a ^ b + b = a ^ b + a = a ^ b + fmt.Println(a) + fmt.Println(b) + + arr := []int{3, 1, 100} + IsoOrSwap(arr, 0, 3) + fmt.Println(arr) + + // i和j指向同一块内存,这种位运算交换变量的方法就不可行了。 + IsoOrSwap(arr, 0, 0) +} +func IsoOrSwap(arr []int, i, j int) { + arr[i] = arr[i] ^ arr[j] + arr[j] = arr[i] ^ arr[j] + arr[i] = arr[i] ^ arr[j] } ``` -==注意,如果a和b指向同一块内存,改方法不可行== +> 注意,如果a和b指向同一块内存,该交换方法不可行 -题目二:一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数 +--- + +**题目二:一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数** > [2,2,1,3,2,3,2,1,1] 数组中存在四个2,两个3,三个1,定义一个常量等于0,分别对该数组中的数遍历一遍进行异或,最后,该变量等于多少,那么奇数的值就是多少。因为异或运算满足交换和结合律 -题目三:怎么把一个int类型的数,提取出最右侧的1来 +```Go +package main + +import "fmt" + +func main() { + arr := []int{2, 2, 1, 3, 2, 3, 2, 1, 1} + flag := 0 + for i := 0; i < len(arr); i++ { + flag = flag ^ arr[i] + } + + fmt.Println(flag) +} +// 打印 1 +``` + +--- + +**题目三:怎么把一个int类型的数,提取出最右侧的1来** > n与上(n取反加1)即可 => N & ( (~N)+1 ) -题目四:一个数组中有两种不相等的数出现了奇数次,其他数出现了偶数次,怎么找到并打印这两种数 +例如一个int数n:00000000 00000000 00001010 01000000 -> 定义一个常量eor = 0,分别对该数组每个数异或,最终结果为a异或b,其中a和b就是这两个奇数,由于a!=b所以a异或b不等于0,即eor的值某一位上一定为1(有可能不止一个1随便选一个例如第八位),用该位做标记对原有数组的数进行分类,那么a和b由于第八位不相同一定被分开,再定义常量eor' = 0分别对第八位为0的数异或,那么得到的值,就是a和b其中一个,由于之前eor = a异或b,那么在用eor和eor'异或,就是另外一个值。一般来说,随便找一个1我们就找最右侧的那个1,如题目三 +先对n取反得到: 11111111 11111111 11110101 10111111 +再加1得到: 11111111 11111111 11110101 11000000 -```Java -package class01; +再与原n做与运算:11111111 11111111 11110101 11000000 & 00000000 00000000 00001010 01000000 -public class Code07_EvenTimesOddTimes { +得到:00000000 00000000 00000000 01000000 - // arr中,只有一种数,出现奇数次 - public static void printOddTimesNum1(int[] arr) { - int eor = 0; - for (int i = 0; i < arr.length; i++) { - eor ^= arr[i]; - } - System.out.println(eor); - } +--- - // arr中,有两种数,出现奇数次 - public static void printOddTimesNum2(int[] arr) { - int eor = 0; - for (int i = 0; i < arr.length; i++) { - eor ^= arr[i]; - } - // eor = a ^ b - // eor != 0 - // eor必然有一个位置上是1 - // 0110010000 - // 0000010000 - int rightOne = eor & (~eor + 1); // 提取出最右的1 - int onlyOne = 0; // eor' - for (int i = 0 ; i < arr.length;i++) { - // arr[i] = 111100011110000 - // rightOne= 000000000010000 - if ((arr[i] & rightOne) != 0) { - onlyOne ^= arr[i]; - } - } - System.out.println(onlyOne + " " + (eor ^ onlyOne)); - } +**题目四:一个数组中有两种不相等的数出现了奇数次,其他数出现了偶数次,怎么找到并打印这两种数** - - public static int bit1counts(int N) { - int count = 0; - - // 011011010000 - // 000000010000 1 - - // 011011000000 - // - - - - while(N != 0) { - int rightOne = N & ((~N) + 1); - count++; - N ^= rightOne; - // N -= rightOne - } - - - return count; - - } - - - public static void main(String[] args) { - int a = 5; - int b = 7; +思路:定义一个常量flag = 0,分别对该数组每个数异或,最终结果为a异或b,其中a和b就是这两个奇数,由于a!=b所以a异或b不等于0,即flag的值某一位上一定为1(有可能不止一个1随便选一个例如第八位),用该位做标记对原有数组的数进行分类,那么a和b由于第八位不相同一定被分开,再定义常量flag' = 0分别对第八位为0的数异或,那么得到的值,就是a和b其中一个,由于之前flag = a异或b,那么在用flag和flag'异或,就是另外一个值。一般来说,随便找一个1我们就找最右侧的那个1,如题目三 - a = a ^ b; - b = a ^ b; - a = a ^ b; - System.out.println(a); - System.out.println(b); +```Go +package main - int[] arr1 = { 3, 3, 2, 3, 1, 1, 1, 3, 1, 1, 1 }; - printOddTimesNum1(arr1); +import "fmt" - int[] arr2 = { 4, 3, 4, 2, 2, 2, 4, 1, 1, 1, 3, 3, 1, 1, 1, 4, 2, 2 }; - printOddTimesNum2(arr2); +func main() { + arr := []int{4, 3, 4, 2, 2, 2, 4, 1, 1, 1, 3, 3, 1, 1, 1, 4, 2, 2} + printNum(arr) +} +func printNum(arr []int) { + flag := 0 + // 经过循环处理,flag等于这两个不相等的且出现奇数次的异或结果;a ^ b + for i := 0; i < len(arr); i++ { + flag = flag ^ arr[i] } + // 由于a != b 所以flag不为0。则flag的二进制位上一定存在1,选最后一位的1 + // 选取办法是,用flag 与 自身取反加1的结果做与运算 + rightOne := flag & ((^flag) + 1) + onlyOne := 0 + // 经过这层循环的筛选,onlyOne等于a或者b其中的一个 + for j := 0; j < len(arr); j++ { + if arr[j]&rightOne != 0 { + onlyOne = onlyOne ^ arr[j] + } + } + result1 := onlyOne + result2 := flag ^ onlyOne + // result1和result2就是数组中不相等的且为奇数的两个未知数a、b + fmt.Println(result1) + fmt.Println(result2) } ``` diff --git "a/02-\351\223\276\350\241\250\343\200\201\346\240\210\343\200\201\351\230\237\345\210\227\343\200\201\351\200\222\345\275\222\343\200\201\345\223\210\345\270\214\350\241\250\343\200\201\351\241\272\345\272\217\350\241\250.md" "b/02-\351\223\276\350\241\250\343\200\201\346\240\210\343\200\201\351\230\237\345\210\227\343\200\201\351\200\222\345\275\222\343\200\201\345\223\210\345\270\214\350\241\250\343\200\201\351\241\272\345\272\217\350\241\250.md" index 9b16561..fe337ae 100644 --- "a/02-\351\223\276\350\241\250\343\200\201\346\240\210\343\200\201\351\230\237\345\210\227\343\200\201\351\200\222\345\275\222\343\200\201\345\223\210\345\270\214\350\241\250\343\200\201\351\241\272\345\272\217\350\241\250.md" +++ "b/02-\351\223\276\350\241\250\343\200\201\346\240\210\343\200\201\351\230\237\345\210\227\343\200\201\351\200\222\345\275\222\343\200\201\345\223\210\345\270\214\350\241\250\343\200\201\351\241\272\345\272\217\350\241\250.md" @@ -4,844 +4,361 @@ ## 1.1 链表 ### 1.1.1 单向链表 -> 单向链表的节点结构(可以实现成泛型) : - -```Java - public class Node { - public int value; - public Node next; - - public Node(int data) { - value = data; - } - - } +> 单向链表的节点结构: + +```Go +// Node 单项链表节点结构 +type Node struct { + V int + Next *Node +} ``` ### 1.1.2 双向链表 -> 双向链表的节点结构(可以实现成功泛型): +> 双向链表的节点结构: -```Java - public static class DoubleNode { - public int value; - public DoubleNode last; - public DoubleNode next; - - public DoubleNode(int data) { - value = data; - } - } +```Go +// DoubleNode 双向链表节点结构 +type DoubleNode struct { + V int + Pre *DoubleNode + Next *DoubleNode +} ``` ### 1.1.3 单双链表简单练习 1. 单链表和双链表如何反转 -> 1 -> 2 -> 3 转换为 3 -> 2 -> 1 - -```Java - -package class02; - -import java.util.ArrayList; - -public class Code01_ReverseList { - - 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; - } - } - - // 翻转单向链表,传入头结点 - 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; - } - - // 翻转双向链表,传入头结点 - public static DoubleNode reverseDoubleList(DoubleNode head) { - DoubleNode pre = null; - DoubleNode next = null; - while (head != null) { - next = head.next; - head.next = pre; - head.last = next; - pre = head; - head = next; - } - return pre; - } - - public static Node testReverseLinkedList(Node head) { - if (head == null) { - return null; - } - ArrayList list = new ArrayList<>(); - while (head != null) { - list.add(head); - head = head.next; - } - list.get(0).next = null; - int N = list.size(); - for (int i = 1; i < N; i++) { - list.get(i).next = list.get(i - 1); - } - return list.get(N - 1); - } - - public static DoubleNode testReverseDoubleList(DoubleNode head) { - if (head == null) { - return null; - } - ArrayList list = new ArrayList<>(); - while (head != null) { - list.add(head); - head = head.next; - } - list.get(0).next = null; - DoubleNode pre = list.get(0); - int N = list.size(); - for (int i = 1; i < N; i++) { - DoubleNode cur = list.get(i); - cur.last = null; - cur.next = pre; - pre.last = cur; - pre = cur; - } - return list.get(N - 1); - } - - public static Node generateRandomLinkedList(int len, int value) { - int size = (int) (Math.random() * (len + 1)); - if (size == 0) { - return null; - } - size--; - Node head = new Node((int) (Math.random() * (value + 1))); - Node pre = head; - while (size != 0) { - Node cur = new Node((int) (Math.random() * (value + 1))); - pre.next = cur; - pre = cur; - size--; - } - return head; - } - - public static DoubleNode generateRandomDoubleList(int len, int value) { - int size = (int) (Math.random() * (len + 1)); - if (size == 0) { - return null; - } - size--; - DoubleNode head = new DoubleNode((int) (Math.random() * (value + 1))); - DoubleNode pre = head; - while (size != 0) { - DoubleNode cur = new DoubleNode((int) (Math.random() * (value + 1))); - pre.next = cur; - cur.last = pre; - pre = cur; - size--; - } - return head; - } - - // 要求无环,有环别用这个函数 - public static boolean checkLinkedListEqual(Node head1, Node head2) { - while (head1 != null && head2 != null) { - if (head1.value != head2.value) { - return false; - } - head1 = head1.next; - head2 = head2.next; - } - return head1 == null && head2 == null; - } - - // 要求无环,有环别用这个函数 - public static boolean checkDoubleListEqual(DoubleNode head1, DoubleNode head2) { - boolean null1 = head1 == null; - boolean null2 = head2 == null; - if (null1 && null2) { - return true; - } - if (null1 ^ null2) { - return false; - } - if (head1.last != null || head2.last != null) { - return false; - } - DoubleNode end1 = null; - DoubleNode end2 = null; - while (head1 != null && head2 != null) { - if (head1.value != head2.value) { - return false; - } - end1 = head1; - end2 = head2; - head1 = head1.next; - head2 = head2.next; - } - if (head1 != null || head2 != null) { - return false; - } - while (end1 != null && end2 != null) { - if (end1.value != end2.value) { - return false; - } - end1 = end1.last; - end2 = end2.last; - } - return end1 == null && end2 == null; - } - - public static void main(String[] args) { - int len = 50; - int value = 100; - int testTime = 100000; - for (int i = 0; i < testTime; i++) { - Node node1 = generateRandomLinkedList(len, value); - Node reverse1 = reverseLinkedList(node1); - Node back1 = testReverseLinkedList(reverse1); - if (!checkLinkedListEqual(node1, back1)) { - System.out.println("oops!"); - break; - } - DoubleNode node2 = generateRandomDoubleList(len, value); - DoubleNode reverse2 = reverseDoubleList(node2); - DoubleNode back2 = testReverseDoubleList(reverse2); - if (!checkDoubleListEqual(node2, back2)) { - System.out.println("oops!"); - break; - } - } - System.out.println("finish!"); - - } - +> 例如单链表:1 -> 2 -> 3 转换为 3 -> 2 -> 1; + + +```Go +// ReverseLinkedList 翻转单链表 +func ReverseLinkedList(head *Node) *Node { + var pre *Node + var next *Node + for head != nil { + // 把当前节点的下一个节点保存到next + next = head.Next + // 当前节点的指向,改为指向前一个节点 + head.Next = pre + // pre 节点按原链表方向向下移动 + pre = head + // head 节点按原链表方向向下移动 + head = next + } + // 按照原链表方向移动,当前节点为nil退出循环的时候,那么pre节点就是原链表的最后一个节点,链表被成功翻转。 + // 当前头结点pre返回 + return pre } +``` +```Go +// ReverseDoubleLinkedList 翻转双向链表 +func ReverseDoubleLinkedList(head *DoubleNode) *DoubleNode { + var pre *DoubleNode + var next *DoubleNode + for head != nil { + // 保留当前节点的next节点的地址 + next = head.Next + // 当前节点的下一个节点指pre + head.Next = pre + // 当前节点的上一个节点指向原链表当前节点的next节点。 + head.Pre = next + // pre 节点按原链表方向向下移动 + pre = head + // head 节点按原链表方向向下移动 + head = next + } + return pre +} ``` 2. 把给定的值都删除 > 比如给定一个链表头结点,删除该节点上值为3的节点,那么可能头结点就是3,存在删头部的情况,这里最终返回应该是删除所有值为3的节点之后的新的头部 -```Java - -package class02; - -public class Code02_DeleteGivenValue { - - public static class Node { - public int value; - public Node next; - - public Node(int data) { - this.value = data; +```Go +// RemoveValue 删除链表中值等于target的节点 +func RemoveValue(head *Node, target int) *Node { + // 处理链表头结点的值即等于target的节点 + for head != nil { + if head.V != target { + break } + head = head.Next } - // 先检查头部,寻找第一个不等于需要删除的值的节点,就是新的头部 - public static Node removeValue(Node head, int 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) { - pre.next = cur.next; - } else { - pre = cur; - } - cur = cur.next; - } - return head; + // 1、链表中的节点值全部都等于target + // 2、原始链表为nil + if head == nil { + return head } + // head来到第一个不需要删除的位置 + pre := head + cur := head + for cur != nil { + // 当前节点cur往下,有多少v等于target的节点,就删除多少节点 + if cur.V == target { + pre.Next = cur.Next + } else { + pre = cur + } + // 当前节点向下滑动 + cur = cur.Next + } + return head } - ``` -> Tips: Java中也有可能产生内存泄漏,与CPP不同,CPP的内存泄漏有可能是我们开辟了内存空间忘记释放。而Java的内存泄漏大可能是程序中的变量的生存周期引起的,如果该程序是一个类似定时任务的7*24小时不间断运行,那么申请的变量(数据结构)就有可能不会被及时释放。如果不注意往里面添加一些不必要的变量,这些变量就是内存泄漏 - ## 1.2 栈、队列 1. 逻辑概念 ->栈:数据先进后出,犹如弹夹 - ->队列: 数据先进先出,排队 - -2. 底层实现方式 +> 栈:数据先进后出,犹如弹夹 -> 双向链表实现 +> 队列: 数据先进先出,排队 -```Java -package class02; +```Go +// 利用双向链表实现双端队列 +package main -import java.util.LinkedList; -import java.util.Queue; -import java.util.Stack; +// DoubleEndsNode 双端队列节点 +type DoubleEndsNode struct { + val int + pre *DoubleEndsNode + next *DoubleEndsNode +} -public class Code03_DoubleEndsQueueToStackAndQueue { +// DoubleEndsList 双端队列接口 +type DoubleEndsList interface { + // AddFromHead 从头部添加节点 + AddFromHead(v int) + // AddFromBottom 从尾部添加节点 + AddFromBottom(v int) + // PopFromHead 从头部弹出节点 + PopFromHead() (int, bool) + // PopFromBottom 从尾部弹出节点 + PopFromBottom() (int, bool) + // IsEmpty 双端队列是否为空 + IsEmpty() bool +} - public static class Node { - public T value; - public Node last; - public Node next; +type DoubleEndsQueue struct { + head *DoubleEndsNode + tail *DoubleEndsNode +} - public Node(T data) { - value = data; - } +func (q *DoubleEndsQueue) AddFromHead(v int) { + cur := &DoubleEndsNode{ + val: v, } - - public static class DoubleEndsQueue { - public Node head; - public Node tail; - - // 从头部加节点 - public void addFromHead(T value) { - Node cur = new Node(value); - if (head == null) { - head = cur; - tail = cur; - } else { - cur.next = head; - head.last = cur; - head = cur; - } - } - - // 从尾部加节点 - public void addFromBottom(T value) { - Node cur = new Node(value); - if (head == null) { - head = cur; - tail = cur; - } else { - cur.last = tail; - tail.next = cur; - tail = cur; - } - } - - // 从头部弹出节点 - public T popFromHead() { - if (head == null) { - return null; - } - Node cur = head; - if (head == tail) { - head = null; - tail = null; - } else { - head = head.next; - cur.next = null; - head.last = null; - } - return cur.value; - } - - // 从尾部弹出节点 - public T popFromBottom() { - if (head == null) { - return null; - } - Node cur = tail; - if (head == tail) { - head = null; - tail = null; - } else { - tail = tail.last; - tail.next = null; - cur.last = null; - } - return cur.value; - } - - // 该双向链表结构是否为空 - public boolean isEmpty() { - return head == null; - } - + if q.head == nil { + q.head = cur + q.tail = cur + } else { + cur.next = q.head + q.head.pre = cur + q.head = cur } +} - // 用上述双向链表结构实现栈 - public static class MyStack { - private DoubleEndsQueue queue; - - public MyStack() { - queue = new DoubleEndsQueue(); - } - - public void push(T value) { - queue.addFromHead(value); - } - - public T pop() { - return queue.popFromHead(); - } - - public boolean isEmpty() { - return queue.isEmpty(); - } - +func (q *DoubleEndsQueue) AddFromBottom(v int) { + cur := &DoubleEndsNode{ + val: v, } - - // 用上述双向链表结构实现队列 - public static class MyQueue { - private DoubleEndsQueue queue; - - public MyQueue() { - queue = new DoubleEndsQueue(); - } - - public void push(T value) { - queue.addFromHead(value); - } - - public T poll() { - return queue.popFromBottom(); - } - - public boolean isEmpty() { - return queue.isEmpty(); - } - + if q.head == nil { + q.head = cur + q.tail = cur + } else { + q.tail.next = cur + cur.pre = q.tail + q.tail = cur } +} - public static boolean isEqual(Integer o1, Integer o2) { - if (o1 == null && o2 != null) { - return false; - } - if (o1 != null && o2 == null) { - return false; - } - if (o1 == null && o2 == null) { - return true; - } - return o1.equals(o2); +func (q *DoubleEndsQueue) PopFromHead() (int, bool) { + if q.head == nil { + return 0, false + } + v := q.head.val + if q.head == q.tail { + q.head = nil + q.tail = nil + return v, true + } else { + h := q.head + q.head = q.head.next + q.head.pre = nil + h.next = nil + return v, true } +} - public static void main(String[] args) { - int oneTestDataNum = 100; - int value = 10000; - int testTimes = 100000; - for (int i = 0; i < testTimes; i++) { - MyStack myStack = new MyStack<>(); - MyQueue myQueue = new MyQueue<>(); - Stack stack = new Stack<>(); - Queue queue = new LinkedList<>(); - for (int j = 0; j < oneTestDataNum; j++) { - int nums = (int) (Math.random() * value); - if (stack.isEmpty()) { - myStack.push(nums); - stack.push(nums); - } else { - if (Math.random() < 0.5) { - myStack.push(nums); - stack.push(nums); - } else { - if (!isEqual(myStack.pop(), stack.pop())) { - System.out.println("oops!"); - } - } - } - int numq = (int) (Math.random() * value); - if (stack.isEmpty()) { - myQueue.push(numq); - queue.offer(numq); - } else { - if (Math.random() < 0.5) { - myQueue.push(numq); - queue.offer(numq); - } else { - if (!isEqual(myQueue.poll(), queue.poll())) { - System.out.println("oops!"); - } - } - } - } - } - System.out.println("finish!"); +func (q *DoubleEndsQueue) PopFromBottom() (int, bool) { + if q.head == nil { + return 0, false + } + v := q.tail.val + if q.head == q.tail { + q.head = nil + q.tail = nil + return v, true + } else { + t := q.tail + q.tail = q.tail.pre + q.tail.next = nil + t.pre = nil + return v, true } - } +func (q *DoubleEndsQueue) IsEmpty() bool { + return q.head == nil +} ``` -> 数组实现,对于栈特别简单,对于队列,如下 - -```Java -package class02; - -public class Code04_RingArray { - - public static class MyQueue { - // 数组结构 - private int[] arr; - // 往当前队列添加数的下标位置 - private int pushi; - // 当前队列需要出队列的位置 - private int polli; - // 当前队列使用的空间大小 - private int size; - // 数组最大大小,用户传入 - private final int limit; - - public MyQueue(int limit) { - arr = new int[limit]; - pushi = 0; - polli = 0; - size = 0; - this.limit = limit; - } +2. 栈、队列的底层实现方式 - public void push(int value) { - if (size == limit) { - throw new RuntimeException("栈满了,不能再加了"); - } - size++; - arr[pushi] = value; - pushi = nextIndex(pushi); - } +> 利用双向链表(双端队列)封装栈和队列 - public int pop() { - if (size == 0) { - throw new RuntimeException("栈空了,不能再拿了"); - } - size--; - int ans = arr[polli]; - polli = nextIndex(polli); - return ans; - } +```Go +// Stack 利用双端队列实现栈 +type Stack struct { + qu *DoubleEndsQueue +} - public boolean isEmpty() { - return size == 0; - } +func (s *Stack) push(v int) { + s.qu.AddFromHead(v) +} - // 如果现在的下标是i,返回下一个位置,该实现可以实现环形的ringbuffer - private int nextIndex(int i) { - return i < limit - 1 ? i + 1 : 0; - } +func (s *Stack) pop() (int, bool) { + return s.qu.PopFromHead() +} +func (s *Stack) peek() (int, bool){ + if s.qu.IsEmpty() { + return 0, false } - + return s.qu.head.val, true } ``` -## 1.3 栈、队列常见面试题 - -一、实现一个特殊的栈,在基本功能的基础上,再实现返回栈中最小元素的功能更 - -1、pop、push、getMin操作的时间复杂度都是O(1) +```Go +// Queue 利用双端队列实现队列 +type Queue struct { + qu *DoubleEndsQueue +} -2、设计的栈类型可以使用现成的栈结构 +func (q *Queue) push(v int) { + q.qu.AddFromHead(v) +} -> 思路:准备两个栈,一个data栈,一个min栈。数据压data栈,min栈对比min栈顶元素,谁小加谁。这样的话data栈和min栈是同步上升的,元素个数一样多,且min栈的栈顶,是data栈所有元素中最小的那个。数据弹出data栈,我们同步弹出min栈,保证个数相等,切min栈弹出的就是最小值 +func (q *Queue) poll() (int, bool) { + return q.qu.PopFromBottom() +} -```Java -package class02; +func (q *Queue)IsEmpty() bool { + return q.qu.IsEmpty() +} +``` -import java.util.Stack; +> 数组实现栈和队列, 对于栈特别简单,略过,对于队列,如下 -public class Code05_GetMinStack { - public static class MyStack1 { - private Stack stackData; - private Stack stackMin; +```Go +package main - public MyStack1() { - this.stackData = new Stack(); - this.stackMin = new Stack(); - } +import "fmt" - public void push(int newNum) { - // 当前最小栈为空,直接压入 - if (this.stackMin.isEmpty()) { - this.stackMin.push(newNum); - // 当前元素小于最小栈的栈顶,压入当前值 - } else if (newNum <= this.getmin()) { - this.stackMin.push(newNum); - } - // 往数据栈中压入当前元素 - this.stackData.push(newNum); - } +type Que struct { + // 队列的底层结构 + arr []int +} - public int pop() { - if (this.stackData.isEmpty()) { - throw new RuntimeException("Your stack is empty."); - } - int value = this.stackData.pop(); - if (value == this.getmin()) { - this.stackMin.pop(); - } - return value; - } +func (q *Que) push (v int) { + q.arr = append(q.arr, v) +} - public int getmin() { - if (this.stackMin.isEmpty()) { - throw new RuntimeException("Your stack is empty."); - } - return this.stackMin.peek(); - } +func (q *Que) poll () (int, bool){ + if len(q.arr) == 0 { + return 0, false } + v := q.arr[0] + q.arr = q.arr[1:] + return v, true +} - public static class MyStack2 { - private Stack stackData; - private Stack stackMin; - - public MyStack2() { - this.stackData = new Stack(); - this.stackMin = new Stack(); - } - - public void push(int newNum) { - if (this.stackMin.isEmpty()) { - this.stackMin.push(newNum); - } else if (newNum < this.getmin()) { - this.stackMin.push(newNum); - } else { - int newMin = this.stackMin.peek(); - this.stackMin.push(newMin); - } - this.stackData.push(newNum); - } - - public int pop() { - if (this.stackData.isEmpty()) { - throw new RuntimeException("Your stack is empty."); - } - // 弹出操作,同步弹出,保证大小一致,只返回给用户data栈中的内容即可 - this.stackMin.pop(); - return this.stackData.pop(); - } - - public int getmin() { - if (this.stackMin.isEmpty()) { - throw new RuntimeException("Your stack is empty."); - } - return this.stackMin.peek(); - } +func main () { + q := Que{} + q.push(1) + q.push(9) + q.push(3) + if poll, ok := q.poll(); ok { + fmt.Println(poll) } - - public static void main(String[] args) { - MyStack1 stack1 = new MyStack1(); - stack1.push(3); - System.out.println(stack1.getmin()); - stack1.push(4); - System.out.println(stack1.getmin()); - stack1.push(1); - System.out.println(stack1.getmin()); - System.out.println(stack1.pop()); - System.out.println(stack1.getmin()); - - System.out.println("============="); - - MyStack1 stack2 = new MyStack1(); - stack2.push(3); - System.out.println(stack2.getmin()); - stack2.push(4); - System.out.println(stack2.getmin()); - stack2.push(1); - System.out.println(stack2.getmin()); - System.out.println(stack2.pop()); - System.out.println(stack2.getmin()); + if poll, ok := q.poll(); ok { + fmt.Println(poll) } - -} -``` - -二、如何用栈结构实现队列结构,如何用队列结构实现栈结构 - -> 这两种结构的应用实在太多,刷题时会大量见到 - -```Java -/** -* 两个栈实现队列 -**/ -package class02; - -import java.util.Stack; - -public class Code06_TwoStacksImplementQueue { - - public static class TwoStacksQueue { - public Stack stackPush; - public Stack stackPop; - - public TwoStacksQueue() { - stackPush = new Stack(); - stackPop = new Stack(); - } - - // push栈向pop栈倒入数据 - private void pushToPop() { - if (stackPop.empty()) { - while (!stackPush.empty()) { - stackPop.push(stackPush.pop()); - } - } - } - - public void add(int pushInt) { - stackPush.push(pushInt); - pushToPop(); - } - - public int poll() { - if (stackPop.empty() && stackPush.empty()) { - throw new RuntimeException("Queue is empty!"); - } - pushToPop(); - return stackPop.pop(); - } - - public int peek() { - if (stackPop.empty() && stackPush.empty()) { - throw new RuntimeException("Queue is empty!"); - } - pushToPop(); - return stackPop.peek(); - } + if poll, ok := q.poll(); ok { + fmt.Println(poll) } - - public static void main(String[] args) { - TwoStacksQueue test = new TwoStacksQueue(); - test.add(1); - test.add(2); - test.add(3); - System.out.println(test.peek()); - System.out.println(test.poll()); - System.out.println(test.peek()); - System.out.println(test.poll()); - System.out.println(test.peek()); - System.out.println(test.poll()); + if poll, ok := q.poll(); ok { + fmt.Println(poll) } - } - ``` -```Java -/** -* 两个队列实现栈 -**/ -package class02; - -import java.util.LinkedList; -import java.util.Queue; -import java.util.Stack; - -public class Code07_TwoQueueImplementStack { +## 1.3 栈、队列常见面试题 - public static class TwoQueueStack { - public Queue queue; - public Queue help; +一、实现一个特殊的栈,在基本功能的基础上,再实现返回栈中最小元素的功能 - public TwoQueueStack() { - queue = new LinkedList<>(); - help = new LinkedList<>(); - } +1、pop、push、getMin操作的时间复杂度都是O(1) - public void push(T value) { - queue.offer(value); - } +2、设计的栈类型可以使用现成的栈结构 - public T poll() { - while (queue.size() > 1) { - help.offer(queue.poll()); - } - T ans = queue.poll(); - Queue tmp = queue; - queue = help; - help = tmp; - return ans; - } +> 思路:准备两个栈,一个data栈,一个min栈。数据压data栈,min栈对比min栈顶元素,谁小加谁。这样的话data栈和min栈是同步上升的,元素个数一样多,且min栈的栈顶,是data栈所有元素中最小的那个。数据弹出data栈,我们同步弹出min栈,保证个数相等,且min栈弹出的就是最小值 - public T peek() { - while (queue.size() > 1) { - help.offer(queue.poll()); - } - T ans = queue.poll(); - help.offer(ans); - Queue tmp = queue; - queue = help; - help = tmp; - return ans; - } +```Go +type MinStack struct { + data *Stack + min *Stack +} - public boolean isEmpty() { - return queue.isEmpty(); +func (s *MinStack) push(v int) { + // min栈只保存最小的v,当然这里也可以设计成min栈和data栈同步上升的策略 + if s.min.IsEmpty() { + s.min.push(v) + } else if c, ok := s.min.peek(); ok { + if v <= c { // 小于等于都入栈,弹出的时候等于也同步弹出min栈 + s.min.push(v) + } else { + s.min.push(c) } - } + // 数据栈稳步上升 + s.data.push(v) +} - public static void main(String[] args) { - System.out.println("test begin"); - TwoQueueStack myStack = new TwoQueueStack<>(); - Stack test = new Stack<>(); - int testTime = 1000000; - int max = 1000000; - for (int i = 0; i < testTime; i++) { - if (myStack.isEmpty()) { - if (!test.isEmpty()) { - System.out.println("Oops"); - } - int num = (int) (Math.random() * max); - myStack.push(num); - test.push(num); - } else { - if (Math.random() < 0.25) { - int num = (int) (Math.random() * max); - myStack.push(num); - test.push(num); - } else if (Math.random() < 0.5) { - if (!myStack.peek().equals(test.peek())) { - System.out.println("Oops"); - } - } else if (Math.random() < 0.75) { - if (!myStack.poll().equals(test.pop())) { - System.out.println("Oops"); - } - } else { - if (myStack.isEmpty() != test.isEmpty()) { - System.out.println("Oops"); - } - } - } +func (s *MinStack) pop() (int, bool) { + if s.data.IsEmpty() { + return 0, false + } + v, _ := s.data.pop() + if m, ok := s.min.peek(); ok { + if m == v { + s.min.pop() } - - System.out.println("test finish!"); - } + return v, true +} +func (s *MinStack) getMin() (int, bool){ + if s.min.IsEmpty() { + return 0, false + } + return s.min.peek() } ``` @@ -859,35 +376,47 @@ public class Code07_TwoQueueImplementStack { 2、 左部分求最大值,右部分求最大值 3、[L...R]范围上的最大值,就是max{左部分最大值,右部分最大值} -==2步骤是个递归过程,当范围上只有一个数,就可以不用再递归了== +> 2步骤是个递归过程,当范围上只有一个数,就可以不用再递归了 -```Java -package class02; +```Go +package main -public class Code08_GetMax { +import "fmt" - // 求arr中的最大值 - public static int getMax(int[] arr) { - return process(arr, 0, arr.length - 1); +func getMax(arr []int) (int, error) { + if len(arr) == 0 { + return 0, fmt.Errorf("arr len is zero") } + return process(arr, 0, len(arr)-1), nil +} - // arr[L..R]范围上求最大值 L ... R N - public static int process(int[] arr, int L, int R) { - if (L == R) { // arr[L..R]范围上只有一个数,直接返回,base case - return arr[L]; - } - int mid = L + ((R - L) >> 1); // 中点 - // 左部分最大值 - int leftMax = process(arr, L, mid); - // 右部分最大值 - int rightMax = process(arr, mid + 1, R); - return Math.max(leftMax, rightMax); +func process(arr []int, l, r int) int { + if l == r { + return arr[l] + } + mid := l + (r-l)/2 + // 左范围最大值 + lm := process(arr, l, mid) + // 右范围最大值 + rm := process(arr, mid+1, r) + if lm > rm { + return lm + } else { + return rm } +} +func main() { + arr := []int{1, 4, 2, 6, 992, 4, 2234, 83} + m, err := getMax(arr) + if err != nil { + panic(err) + } + fmt.Println(m) } ``` -> 递归在系统中是怎么实现的?递归实际上利用的是系统栈来实现的。保存当前调用现场,去执行子问题,子问题的返回作为现场的需要的参数填充,最终构建还原栈顶的现场,返回。所以递归行为不是玄学,任何递归都可以改为非递归实现,我们自己压栈用迭代等实现就行 +> 递归在系统中是怎么实现的?递归实际上利用的是系统栈来实现的。保存当前调用现场,去执行子问题,子问题的返回作为现场的需要的参数填充,最终构建还原栈顶的现场,返回。任何递归都可以改为非递归实现,需要我们自己压栈用迭代等实现 ### 1.4.1 递归行为的时间复杂度 @@ -925,11 +454,7 @@ logb^a == d => O(N^d * logN) > Hash表的增删改查,在使用的时候,一律认为时间复杂度是O(1)的 -> 在Java中,int double float基础类型,按值传递; Integer, Double, Float按引用传递的,比较包装类型的值是否相等,使用equals方法。 - -==注意:在Java底层,包装类如果范围比较小,底层仍然采用值传递,比如Integer如果范围在-128~127之间,是按值传递的== - -==但是在Hash表中,即使是包装类型的key,我们也一律按值传递,例如Hash如果我们put相同的key的值,那么不会产生两个值相等的key而是覆盖操作。但是Hash表并不是一直是按值传递的,只是针对包装类型,如果是我们自定义的引用类型,那么仍然按引用传递== +> Golang中hashMap的结构为map,hashSet可以由map结构进行简单改造即可实现 ## 1.6 顺序表 TreeMap、TreeSet @@ -937,119 +462,85 @@ logb^a == d => O(N^d * logN) > 有序表的底层可以有很多结构实现,比如AVL树,SB树,红黑树,跳表。其中AVL,SB,红黑都是具备各自平衡性的搜索二叉树 -> 由于平衡二叉树每时每刻都会维持自身的平衡,所以操作为O(logN)。暂时理解,后面会单独整理 +> 由于平衡二叉树每时每刻都会维持自身的平衡,所以操作为O(logN)。后面篇幅会单独介绍平衡二叉树。 -> 由于满足去重排序功能来维持底层树的平衡,所以如果是基础类型和包装类型的key直接按值来做比较,但是如果我们的key是自己定义的类型,那么我们要自己制定比较规则(比较器),用来让底层的树保持比较后的平衡 +> 由于满足去重排序功能来维持底层树的平衡,所以如果是基础类型key直接按值来做比较,但是如果我们的key是自己定义的结构体类型,那么我们要自己制定比较规则,Go中为实现sort的接口,用来让底层的树保持比较后的平衡 -```Java -package class02; +```Go +// Go中HashSet的简单实现 +package set -import java.util.HashMap; -import java.util.HashSet; -import java.util.TreeMap; +type Set interface { + Add(elements ...interface{}) + Remove(elements ...interface{}) + Contains(elements ...interface{}) bool +} -public class HashMapAndSortedMap { - - - public static class Node{ - public int value; - public Node(int v) { - value = v; - } +var itemExists = struct{}{} + +type HashSet struct { + items map[interface{}]struct{} +} + +func New(values ...interface{}) *HashSet { + set := &HashSet{items: make(map[interface{}]struct{})} + if len(values) > 0 { + set.Add(values...) } - - public static void main(String[] args) { - // UnSortedMap - HashMap map = new HashMap<>(); - map.put(1000000, "我是1000000"); - map.put(2, "我是2"); - map.put(3, "我是3"); - map.put(4, "我是4"); - map.put(5, "我是5"); - map.put(6, "我是6"); - map.put(1000000, "我是1000001"); - - System.out.println(map.containsKey(1)); - System.out.println(map.containsKey(10)); - - System.out.println(map.get(4)); - System.out.println(map.get(10)); - - map.put(4, "他是4"); - System.out.println(map.get(4)); - - map.remove(4); - System.out.println(map.get(4)); - - - - // key - HashSet set = new HashSet<>(); - set.add("abc"); - set.contains("abc"); - set.remove("abc"); - - // 哈希表,增、删、改、查,在使用时,O(1) - - System.out.println("====================="); - - int a = 100000; - int b = 100000; - System.out.println(a == b); - - Integer c = 100000; - Integer d = 100000; - System.out.println(c.equals(d)); - - Integer e = 127; // - 128 ~ 127 - Integer f = 127; - System.out.println(e == f); - - - - HashMap map2 = new HashMap<>(); - Node node1 = new Node(1); - Node node2 = node1; - map2.put(node1, "我是node1"); - map2.put(node2, "我是node1"); - System.out.println(map2.size()); - - System.out.println("======================"); - - TreeMap treeMap = new TreeMap<>(); - - treeMap.put(3, "我是3"); - treeMap.put(4, "我是4"); - treeMap.put(8, "我是8"); - treeMap.put(5, "我是5"); - treeMap.put(7, "我是7"); - treeMap.put(1, "我是1"); - treeMap.put(2, "我是2"); - - System.out.println(treeMap.containsKey(1)); - System.out.println(treeMap.containsKey(10)); - - System.out.println(treeMap.get(4)); - System.out.println(treeMap.get(10)); - - treeMap.put(4, "他是4"); - System.out.println(treeMap.get(4)); - - treeMap.remove(4); - System.out.println(treeMap.get(4)); - - System.out.println(treeMap.firstKey()); - System.out.println(treeMap.lastKey()); - // <= 4 - System.out.println(treeMap.floorKey(4)); - // >= 4 - System.out.println(treeMap.ceilingKey(4)); - // O(logN) - + return set +} + +func (set *HashSet) Add(items ...interface{}) { + for _, item := range items { + set.items[item] = itemExists } } + +func (set *HashSet) Remove(items ...interface{}) { + for _, item := range items { + delete(set.items, item) + } +} + +func (set *HashSet) Contains(items ...interface{}) bool { + for _, item := range items { + if _, contains := set.items[item]; !contains { + return false + } + } + return true +} ``` +```Go +// Go中基于红黑树TreeSet的简单使用 +package main +import ( + "fmt" + "github.com/emirpasic/gods/sets/treeset" +) +// treeSet => 去重排序 +func main() { + set := treeset.NewWithIntComparator() + set.Add() + set.Add(1) + set.Add(2) + set.Add(2, 3) + set.Add() + set.Add(6) + set.Add(4) + if actualValue := set.Empty(); actualValue != false { + fmt.Printf("Got %v expected %v", actualValue, false) + } + if actualValue := set.Size(); actualValue != 3 { + fmt.Printf("Got %v expected %v", actualValue, 3) + } + if actualValue, expectedValue := fmt.Sprintf("%d%d%d", set.Values()...), "12346"; actualValue != expectedValue { + fmt.Printf("Got %v expected %v", actualValue, expectedValue) + } + fmt.Println(set.Values()...) +} +``` \ No newline at end of file diff --git "a/03-\345\275\222\345\271\266\346\216\222\345\272\217\343\200\201\351\232\217\346\234\272\345\277\253\346\216\222.md" "b/03-\345\275\222\345\271\266\346\216\222\345\272\217\343\200\201\351\232\217\346\234\272\345\277\253\346\216\222.md" index aeec6b0..0ab98a5 100644 --- "a/03-\345\275\222\345\271\266\346\216\222\345\272\217\343\200\201\351\232\217\346\234\272\345\277\253\346\216\222.md" +++ "b/03-\345\275\222\345\271\266\346\216\222\345\272\217\343\200\201\351\232\217\346\234\272\345\277\253\346\216\222.md" @@ -33,167 +33,136 @@ > 分组变为a=2*4=8,...直至a>=n,整体有序 -```Java -package class03; +```Go +// mergeSort归并排序递归实现 +func mergeSort(arr []int) { + // 空数组或者只存在1个元素 + if len(arr) < 2 { + return + } -public class Code01_MergeSort { + // 传入被排序数组,以及左右边界到递归函数 + process(arr, 0, len(arr)-1) +} - // 递归方法实现 - public static void mergeSort1(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - // 传入被排序数组,以及左右边界 - process(arr, 0, arr.length - 1); +// process 使得数组arr的L到R位置变为有序 +func process(arr []int, L, R int) { + if L == R { // base case + return } - // arr[L...R]范围上,变成有序的 - // L...R N T(N) = 2*T(N/2) + O(N) -> - public static void process(int[] arr, int L, int R) { - if (L == R) { // base case - return; + mid := L + (R-L)/2 + process(arr, L, mid) + process(arr, mid+1, R) + // 当前栈顶左右已经排好序,准备左右merge,注意这里的merge动作递归的每一层都会调用 + merge(arr, L, mid, R) +} + +// merge arr L到M有序 M+1到R有序 变为arr L到R整体有序 +func merge(arr []int, L, M, R int) { + // merge过程申请辅助数组,准备copy + help := make([]int, 0) + p1 := L + p2 := M + 1 + // p1未越界且p2未越界 + for p1 <= M && p2 <= R { + if arr[p1] <= arr[p2] { + help = append(help, arr[p1]) + p1++ + } else { + help = append(help, arr[p2]) + p2++ } - // >> 有符号右移1位,相当于除以2 - int mid = L + ((R - L) >> 1); - process(arr, L, mid); - process(arr, mid + 1, R); - // 当前栈顶左右已经排好序,准备左右merge,注意这里的merge递归的每一层都会调用 - merge(arr, L, mid, R); } - public static void merge(int[] arr, int L, int M, int R) { - // merge过程申请辅助数组,准备copy - int[] help = new int[R - L + 1]; - // 用来标识help的下标 - int i = 0; - // 左边有序数组的指针 - int p1 = L; - // 右边有序数组的指针 - int p2 = M + 1; - // p1和p2都没越界的情况下,谁小copy谁 - while (p1 <= M && p2 <= R) { - help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++]; - } - // 要么p1越界了,要么p2越界了,谁没越界把谁剩下的元素copy到help中 - while (p1 <= M) { - help[i++] = arr[p1++]; - } - while (p2 <= R) { - help[i++] = arr[p2++]; - } - // 把辅助数组中整体merge后的有序数组,copy回原数组中去 - for (i = 0; i < help.length; i++) { - arr[L + i] = help[i]; - } + // p2越界的情况 + for p1 <= M { + help = append(help, arr[p1]) + p1++ } - // 非递归方法实现 - public static void mergeSort2(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - int N = arr.length; - // 当前有序的,左组长度,那么实质分组大小是从2开始的 - int mergeSize = 1; - while (mergeSize < N) { // log N - // L表示当前分组的左组的位置,初始为第一个分组的左组位置为0 - int L = 0; - // 0.... - while (L < N) { - // L...M 当前左组(mergeSize) - int M = L + mergeSize - 1; - // 当前左组包含当前分组的所有元素,即没有右组了,无需merge已经有序 - if (M >= N) { - break; - } - // L...M为左组 M+1...R(mergeSize)为右组。右组够mergeSize个的时候,右坐标为M + mergeSize,右组不够的情况下右组边界坐标为整个数组右边界N - 1 - int R = Math.min(M + mergeSize, N - 1); - // 把当前组进行merge - merge(arr, L, M, R); - // 下一个分组的左组起始位置 - L = R + 1; - } - // 如果mergeSize乘2必定大于N,直接break。防止mergeSize溢出,有可能N很大,下面乘2有可能范围溢出(整形数大于21亿) - if (mergeSize > N / 2) { - break; - } - // 无符号左移,相当于乘以2 - mergeSize <<= 1; - } + // p1越界的情况 + for p2 <= R { + help = append(help, arr[p2]) + p2++ } - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - return arr; + // 把辅助数组help中整体merge后的有序数组,copy回原数组arr中去 + for j := 0; j < len(help); j++ { + arr[L+j] = help[j] } +} +``` - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; +```Go +// 归并排序非递归实现 +func mergeSort2(arr []int) { + if len(arr) < 2 { + return + } + + N := len(arr) + // 当前有序的,左组长度, 那么实质分组大小是从2开始的 + mergeSize := 1 + for mergeSize < N { + // L表示当前分组的左组的位置,初始为第一个分组的左组位置为0 + L := 0 + for L < N { + // L...M 当前左组(mergeSize) + M := L + mergeSize - 1 + // 当前左组包含当前分组的所有元素,即没有右组了,无需merge已经有序 + if M >= N { + break + } + // L...M为左组 M+1...R(mergeSize)为右组。 + // 右组够mergeSize个的时候,右坐标为M + mergeSize,右组不够的情况下右组边界坐标为整个数组右边界N - 1 + R := math.Min(float64(M+mergeSize), float64(N-1)) + // 把当前组进行merge + merge(arr, L, M, int(R)) + L = int(R) + 1 } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; + // 如果mergeSize乘2必定大于N,直接break。 + // 防止mergeSize溢出,有可能N很大,下面乘2有可能范围溢出(整形数大于21亿) + if mergeSize > N/2 { + break } - return res; + mergeSize *= 2 } +} - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; +// merge arr L到M有序 M+1到R有序 变为arr L到R整体有序 +func merge(arr []int, L, M, R int) { + // merge过程申请辅助数组,准备copy + help := make([]int, 0) + p1 := L + p2 := M + 1 + // p1未越界且p2未越界 + for p1 <= M && p2 <= R { + if arr[p1] <= arr[p2] { + help = append(help, arr[p1]) + p1++ + } else { + help = append(help, arr[p2]) + p2++ } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; } - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); + // p2越界的情况 + for p1 <= M { + help = append(help, arr[p1]) + p1++ } - // for test - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 100; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - mergeSort1(arr1); - mergeSort2(arr2); - if (!isEqual(arr1, arr2)) { - succeed = false; - printArray(arr1); - printArray(arr2); - break; - } - } - System.out.println(succeed ? "Nice!" : "Oops!"); + // p1越界的情况 + for p2 <= R { + help = append(help, arr[p2]) + p2++ } + // 把辅助数组help中整体merge后的有序数组,copy回原数组arr中去 + for j := 0; j < len(help); j++ { + arr[L+j] = help[j] + } } ``` @@ -207,15 +176,15 @@ T(N) = 2T(N/2) + O(N) => O(N*logN) > 非递归复杂度计算,mergeSize*2等于分组从2->4->8->...,每个分组下执行merge操作O(N)。所以非递归和递归的时间复杂度相同,也为O(N)*O(logN) = O(NlogN) -==递归和非递归的归并排序时间复杂度都为O(NlogN)== +> 所以递归和非递归的归并排序时间复杂度都为O(NlogN) -Tips: 为什么选择,冒泡,插入排序的时间复杂度为O(N^2)而归并排序时间复杂度为O(NlogN),因为选择,冒泡,插入排序的每个元素浪费了大量的比较行为N次。而归并没有浪费比较行为,每次比较的结果有序后都会保存下来,最终merge +Tips: 为什么选择,冒泡,插入排序的时间复杂度为O(N^2)而归并排序时间复杂度为O(NlogN),因为选择,冒泡,插入排序的每个元素浪费了大量的比较行为,N次。而归并没有浪费比较行为,每次比较的结果有序后都会保存下来,最终merge ### 1.1.4 归并面试题 1、在一个数组中,一个数左边比它小的数的总和,叫做小和,所有数的小和累加起来,叫做数组的小和。求数组的小和。例如[1, 3, 4, 2, 5] -```text +``` 1左边比1小的数:没有 3左边比3小的数:1 @@ -233,148 +202,72 @@ Tips: 为什么选择,冒泡,插入排序的时间复杂度为O(N^2)而归 > 归并排序解法思路:O(NlogN)。在递归merge的过程中,产生小和。规则是左组比右组数小的时候产生小和,除此之外不产生;当左组和右组数相等的时候,拷贝右组的数,不产生小和;当左组的数大于右组的时候,拷贝右组的数,不产生小和。实质是把找左边比本身小的数的问题,转化为找这个数右侧有多少个数比自己大,在每次merge的过程中,一个数如果处在左组中,那么只会去找右组中有多少个数比自己大 -```Java -package class03; - -public class Code02_SmallSum { - - public static int smallSum(int[] arr) { - if (arr == null || arr.length < 2) { - return 0; - } - return process(arr, 0, arr.length - 1); +```Go +// smallSum 数组小和问题 +func smallSum(arr []int) int { + if len(arr) < 2 { + return 0 } - // arr[L..R]既要排好序,也要求小和返回 - // 所有merge时,产生的小和,累加 - // 左 排序 merge - // 右 排序 merge - // arr 整体 merge - public static int process(int[] arr, int l, int r) { - // 只有一个数,不存在右组,小和为0 - if (l == r) { - return 0; - } - // l < r - int mid = l + ((r - l) >> 1); - // 左侧merge的小和+右侧merge的小和+整体左右两侧的小和 - return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r); - } + return sSum(arr, 0, len(arr) - 1) +} - public static int merge(int[] arr, int L, int m, int r) { - // 在归并排序的基础上改进,增加小和res = 0 - int[] help = new int[r - L + 1]; - int i = 0; - int p1 = L; - int p2 = m + 1; - int res = 0; - while (p1 <= m && p2 <= r) { - // 当前的数是比右组小的,产生右组当前位置到右组右边界数量个小和,累加到res。否则res加0 - res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0; - // 只有左组当前数小于右组copy左边的,否则copy右边的 - help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; - } - while (p1 <= m) { - help[i++] = arr[p1++]; - } - while (p2 <= r) { - help[i++] = arr[p2++]; - } - for (i = 0; i < help.length; i++) { - arr[L + i] = help[i]; - } - return res; +// arr[L..R]既要排好序,也要求小和返回 +// 所有merge时,产生的小和,累加 +// 左 排序 merge +// 右 排序 merge +// arr 整体 merge +func sSum(arr []int, l, r int) int { + // 只有一个数,不存在右组,小和为0 + if l == r { + return 0 } - // for test - public static int comparator(int[] arr) { - if (arr == null || arr.length < 2) { - return 0; - } - int res = 0; - for (int i = 1; i < arr.length; i++) { - for (int j = 0; j < i; j++) { - res += arr[j] < arr[i] ? arr[j] : 0; - } - } - return res; - } + mid := l + (r - l) / 2 + // 左侧merge的小和+右侧merge的小和+整体左右两侧的小和 + return sSum(arr, l, mid) + sSum(arr, mid + 1, r) + sumMerge(arr, l, mid, r); +} - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); +func sumMerge(arr []int, L, M, R int) int { + // merge过程申请辅助数组,准备copy + help := make([]int, 0) + p1 := L + p2 := M + 1 + res := 0 + // p1未越界且p2未越界 + for p1 <= M && p2 <= R { + // 当前的数是比右组小的,产生右组当前位置到右组右边界数量个小和,累加到res。否则res加0 + if arr[p1] < arr[p2] { + help = append(help, arr[p1]) + res += (R - p2 + 1) * arr[p1] + p1++ + } else { + help = append(help, arr[p2]) + res += 0 + p2++ } - return arr; } - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; - } - return res; + // p2越界的情况 + for p1 <= M { + help = append(help, arr[p1]) + p1++ } - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; + // p1越界的情况 + for p2 <= R { + help = append(help, arr[p2]) + p2++ } - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); + // 把辅助数组help中整体merge后的有序数组,copy回原数组arr中去 + for j := 0; j < len(help); j++ { + arr[L+j] = help[j] } - - // for test - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 100; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - if (smallSum(arr1) != comparator(arr2)) { - succeed = false; - printArray(arr1); - printArray(arr2); - break; - } - } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - } - + return res } ``` -类似题目:求一个数组中的所有降序对,例如[3,1,7,0,2]降序对为:(3,1), (3,0), (3,2), (1,0), (70), (7,2)。也可以借助归并排序来解决。实质就是要求一个数右边有多少个数比自身小 - > 什么样的题目以后可以借助归并排序:纠结每个数右边(左边)有多少个数比自身大,比自身小等。求这种数的数量等等 ## 1.2 快排 @@ -389,34 +282,34 @@ public class Code02_SmallSum { 2、 arr[i] > num, 不做操作,i++ -> 给定一个数组,和一个整数num。请把小于num的数放在数组的左边,等于num的放中间,大于num的放右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。[3,5,4,0,4,6,7,2],num=4。实质是经典荷兰国旗问题 +> 给定一个数组,和一个整数num。请把小于num的数放在数组的左边,等于num的放中间,大于num的放右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。[3,5,4,0,4,6,7,2],num=4。该问题实质就是经典的荷兰国旗问题 -思路:设计一个小于区域,下标为-1。设计一个大于区域,下表为arr.length,越界位置。 +思路:设计一个小于区域,下标为-1。设计一个大于区域,下表为arr.length, 数组的左右越界位置。 -1、 如果arr[i]当前位置的数==num, i++直接跳下一个 +1、 如果arr[i]等于当前位置的数num, i++直接跳下一个。间接的扩大了等于区域 -2、 如果arr[i]当前位置的数< num,当前位置的数arr[i]和小于区域的右一个交换,小于区域右扩一个位置,当前位置i++ +2、 如果arr[i]当前位置的数小于num,当前位置的数arr[i]和小于区域的右一个交换,小于区域右扩一个位置,当前位置i++ -3、 如果arr[i]当前位置的数> num,当前位置的数arr[i]与大于区域的左边一个交换,大于区域左移一个位置,i停在原地不做处理,这里不做处理是因为当前位置的数是刚从大于区域交换过来的数,还没做比较 +3、 如果arr[i]当前位置的数大于num,当前位置的数arr[i]与大于区域的左边一个交换,大于区域左移一个位置,i停在原地不做处理,这里不做处理是因为当前位置的数是刚从大于区域交换过来的数,还没做比较 -4、i和大于区域的边界相遇,停止操作 +4、当i和大于区域的边界相遇,停止操作 ### 1.2.2 快排1.0:每次partion搞定一个位置 -思路:在给定数组上做partion,选定数组最右侧的位置上的数作为num,小于num的放在该数组的左边,大于num的放在该数组的右边。完成之后,把该数组最右侧的数组num,交换到大于num区域的第一个位置,确保了交换后的num是小于等于区域的最后一个数(该数直至最后可以保持当前位置不变,属于已经排好序的数),把该num左侧和右侧的数分别进行同样的partion操作(递归)。相当于每次partion搞定一个数的位置,代码实现quickSort1 +思路:在给定数组上做partion, 选定数组最右侧的位置上的数作为num,小于num的放在该数组的左边,大于num的放在该数组的右边。完成之后,把该数组最右侧的数组num,交换到大于num区域的第一个位置,确保了交换后的num是小于等于区域的最后一个数(该数直至最后可以保持当前位置不变,属于已经排好序的数),把该num左侧和右侧的数分别进行同样的partion操作(递归)。相当于每次partion搞定一个数的位置,代码实现quickSort1 ### 1.2.3 快排2.0:每次partion搞定一批位置 -思路:借助荷兰国旗问题的思路,把arr进行partion,把小于num的数放左边,等于放中间,大于放右边。递归时把小于num的区域和大于num的区域做递归,等于num的区域不做处理。相当于每次partion搞定一批数,与标记为相等的数。代码实现quickSort2 +思路:借助荷兰国旗问题的思路,把arr进行partion,把小于num的数放左边,等于放中间,大于放右边。递归时把小于num的区域和大于num的区域做递归,等于num的区域不做处理。相当于每次partion搞定一批数,该批数都与标记数相等。代码实现quickSort2 > 第一版和第二版的快排时间复杂度相同O(N^2):用最差情况来评估,本身有序,每次partion只搞定了一个数是自身,进行了N次partion ### 1.2.4 快排3.0:随机位置作为num标记位 -==随机选一个位置i,让arr[i]和arr[R]交换,再用=arr[R]作为标记位。剩下的所有过程跟快排2.0一样。即为最经典的快排,时间复杂度为O(NlogN)== +> 随机选一个位置i,让arr[i]和arr[R]交换,再选取arr[R]的值作为标记位。剩下的所有过程跟快排2.0一样。即为最经典的快排,时间复杂度为O(NlogN) -> 为什么随机选择标记为时间复杂度就由O(N^2)变为O(NlogN)?如果我们随机选择位置那么就趋向于标记位的左右两侧的递归规模趋向于N/2。那么根据master公式,可以计算出算法复杂度为O(NlogN)。实质上,在我们选择随机的num时,最差情况,最好情况,其他各种情况的出现概率为1/N。对于这N种情况,数学上算出的时间复杂度最终期望是O(NlogN),这个数学上可以进行证明,比较复杂 +> 为什么随机选择标记位的时间复杂度由原本不随机的O(N^2)变为O(NlogN)了呢? 如果我们随机选择位置那么就趋向于标记位的左右两侧的递归规模趋向于N/2。那么根据master公式,可以计算出算法复杂度为O(NlogN)。实质上,在我们选择随机的num时,最差情况,最好情况,其他各种情况的出现概率为1/N。对于这N种情况,数学上算出的时间复杂度最终期望是O(NlogN),这个数学上可以进行证明,证明相对较复杂 > 例如我们的num随机到数组左侧三分之一的位置,那么master公式为 @@ -424,7 +317,7 @@ public class Code02_SmallSum { T(N) = T((1/3)N) + T((2/3)N) + O(N) ``` -> 对于这个递归表达式,master公式是解不了的,master公式只能解决子问题规模一样的递归。对于这个递归,算法导论上给出了计算方法,大致思路为假设一个复杂度,看这个公式是否收敛于这个复杂度的方式,比较麻烦 +> 对于这个递归表达式,master公式是解不了的,master公式只能解决子问题规模一样的递归。对于这个递归,算法导论上给出了计算方法,大致思路为假设一个复杂度,看这个公式是否收敛于这个复杂度的方式 ### 1.2.5 快排的时间复杂度与空间复杂度 @@ -432,195 +325,113 @@ T(N) = T((1/3)N) + T((2/3)N) + O(N) > 空间复杂度:O(logN)。空间复杂度产生于每次递归partion之后,我们需要申请额外的空间变量保存相等区域的左右两侧的位置。那么每次partion需要申请两个变量,多少次partion?实质是该递归树被分了多少层,树的高度,有好有坏,最好logN,最差N。随机选择num之后,期望仍然是概率累加,收敛于O(logN)。 -```Java -package class03; - -public class Code03_PartitionAndQuickSort { - - public static void swap(int[] arr, int i, int j) { - int tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } - - // partion问题 - public static int partition(int[] arr, int L, int R) { - if (L > R) { - return -1; - } - if (L == R) { - return L; - } - int lessEqual = L - 1; - int index = L; - while (index < R) { - if (arr[index] <= arr[R]) { - swap(arr, index, ++lessEqual); - } - index++; - } - swap(arr, ++lessEqual, R); - return lessEqual; - } +```Go +package main - // arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值 - // 小于arr[R]放左侧 等于arr[R]放中间 大于arr[R]放右边 - // 返回中间区域的左右边界 - public static int[] netherlandsFlag(int[] arr, int L, int R) { - // 不存在荷兰国旗问题 - if (L > R) { - return new int[] { -1, -1 }; - } - // 已经都是等于区域,由于用R做划分返回R位置 - if (L == R) { - return new int[] { L, R }; - } - int less = L - 1; // < 区 右边界 - int more = R; // > 区 左边界 - int index = L; - while (index < more) { - // 当前值等于右边界,不做处理,index++ - if (arr[index] == arr[R]) { - index++; - // 小于交换当前值和左边界的值 - } else if (arr[index] < arr[R]) { - swap(arr, index++, ++less); - // 大于右边界的值 - } else { - swap(arr, index, --more); - } - } - // 比较完之后,把R位置的数,调整到等于区域的右边,至此大于区域才是真正意义上的大于区域 - swap(arr, more, R); - return new int[] { less + 1, more }; - } +// swap 交换数组中的两个位置的数 +func swap(arr []int, i, j int) { + tmp := arr[i] + arr[i] = arr[j] + arr[j] = tmp +} - public static void quickSort1(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - process1(arr, 0, arr.length - 1); +// partition 对数组进行partition处理 +func partition(arr []int, L, R int) int { + if L > R { + return -1 } - - public static void process1(int[] arr, int L, int R) { - if (L >= R) { - return; - } - // L..R上partition 标记位为arr[R] 数组被分成 [ <=arr[R] arr[R] >arr[R] ],M为partion之后标记位处在的位置 - int M = partition(arr, L, R); - process1(arr, L, M - 1); - process1(arr, M + 1, R); + if L == R { + return L } - - public static void quickSort2(int[] arr) { - if (arr == null || arr.length < 2) { - return; + // 选定左边界的左边一个位置作为小于区域的起点 + lessEqual := L - 1 + index := L + // 每次搞定一个位置 + for index < R { + if arr[index] <= arr[R] { + lessEqual++ + swap(arr, index, lessEqual) } - process2(arr, 0, arr.length - 1); - } - - public static void process2(int[] arr, int L, int R) { - if (L >= R) { - return; - } - // 每次partion返回等于区域的范围 - int[] equalArea = netherlandsFlag(arr, L, R); - // 对等于区域左边的小于区域递归,partion - process2(arr, L, equalArea[0] - 1); - // 对等于区域右边的大于区域递归,partion - process2(arr, equalArea[1] + 1, R); + index++ } + lessEqual++ + swap(arr, lessEqual, R) + return lessEqual +} - public static void quickSort3(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - process3(arr, 0, arr.length - 1); - } +// arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值 +// 小于arr[R]放左侧 等于arr[R]放中间 大于arr[R]放右边 +// 返回中间区域的左右边界 +func netherlandsFlag(arr []int, L, R int) []int { + // 不存在荷兰国旗问题 + if L > R { + return []int{-1, -1} + } + + // 已经都是等于区域,由于用R做划分返回R位置 + if L == R { + return []int{L, R} + } + + // < 区 右边界 + less := L - 1 + // > 区 左边界 + more := R + index := L + for index < more { + // 当前值等于右边界,不做处理,index++ + if arr[index] == arr[R] { + index++ + } else if arr[index] < arr[R] { // 小于交换当前值和左边界的值 + less++ + swap(arr, index, less) + index++ + } else { // 大于右边界的值 + more-- + swap(arr, index, more) + } + } + // 比较完之后,把R位置的数,调整到等于区域的右边,至此大于区域才是真正意义上的大于区域 + swap(arr, more, R) + return []int{less + 1, more} +} - public static void process3(int[] arr, int L, int R) { - if (L >= R) { - return; - } - // 随机选择位置,与arr[R]上的数做交换 - swap(arr, L + (int) (Math.random() * (R - L + 1)), R); - int[] equalArea = netherlandsFlag(arr, L, R); - process3(arr, L, equalArea[0] - 1); - process3(arr, equalArea[1] + 1, R); +func QuickSort1(arr []int) { + if len(arr) < 2 { + return } - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - return arr; - } + sortByPartition(arr, 0, len(arr)-1) +} - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; - } - return res; +func sortByPartition(arr []int, L int, R int) { + if L >= R { + return } - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; - } + // L到R上进行partition 标记位为arr[R] 数组被分成 [ <=arr[R] arr[R] >arr[R] ],M为partition之后标记位处在的位置 + M := partition(arr, L, R) + sortByPartition(arr, L, M-1) + sortByPartition(arr, M+1, R) +} - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); +func QuickSort2(arr []int) { + if len(arr) < 2 { + return } + sortByNetherlandsFlag(arr, 0, len(arr)-1) +} - // for test - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 100; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - int[] arr3 = copyArray(arr1); - quickSort1(arr1); - quickSort2(arr2); - quickSort3(arr3); - if (!isEqual(arr1, arr2) || !isEqual(arr2, arr3)) { - succeed = false; - break; - } - } - System.out.println(succeed ? "Nice!" : "Oops!"); - +func sortByNetherlandsFlag(arr []int, L int, R int) { + if L >= R { + return } + // 每次partition返回等于区域的范围,荷兰国旗问题 + equalArea := netherlandsFlag(arr, L, R) + // 对等于区域左边的小于区域递归,partition + sortByNetherlandsFlag(arr, L, equalArea[0]-1) + // 对等于区域右边的大于区域递归,partition + sortByNetherlandsFlag(arr, equalArea[1]+1, R) } ``` \ No newline at end of file diff --git "a/04-\345\240\206\343\200\201\347\273\223\346\236\204\344\275\223\346\216\222\345\272\217.md" "b/04-\345\240\206\343\200\201\347\273\223\346\236\204\344\275\223\346\216\222\345\272\217.md" new file mode 100644 index 0000000..8d93148 --- /dev/null +++ "b/04-\345\240\206\343\200\201\347\273\223\346\236\204\344\275\223\346\216\222\345\272\217.md" @@ -0,0 +1,370 @@ +[TOC] +# 1 比较器与堆 + +## 1.1 堆结构 + +### 1.1.1 完全二叉树结构 + +> 完全二叉树结构:要么本层是满的,要么先满左边的,以下都是完全二叉树 + +1. + +``` +graph TD +A-->B +A-->C +``` + +2. + +``` +graph TD +A-->B +A-->C +B-->D +B-->E +C-->F +``` + + +### 1.1.2 数组实现堆 + +- 堆结构就是用数组实现的完全二叉树结构 + + +> 用数组实现完全二叉树结构,从数组下标0开始,当成依次补齐二叉树结构的数据 + +``` +graph TD +0--> 1 +0--> 2 +1--> 3 +1-->4 +2-->5 +2-->6 +``` + +某位置i的左孩子下标为: +```math +lchild = 2*i + 1 +``` +某位置i的右孩子的下标为: +```math +rchild = 2*i + 2 +``` +某位置i的父节点位置为: +```math +parent = (i-1) / 2 +``` + +> 当我们不使用数组的0下标,从1位置开始构建完全二叉树时,方便使用位操作: + +某位置i的左孩子下标为: +```math +lchild = 2*i <==> i << 1 +``` +某位置i的右孩子的下标为: +```math +rchild = 2*i + 1 <==> (i << 1) | 1 +``` +某位置i的父节点位置为: +```math +parent = i / 2 <==> i >> 1 +``` +### 1.1.3 大根堆与小根堆 + +- 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆 + +- 完全二叉树中如果每颗子树的最小值都在顶部就是小根堆 + +**我们认为堆就是大根堆或者小根堆,既不是大根堆也不是小根堆的完全二叉树只是完全二叉树,不能称之为堆** + + + +### 1.1.4 构建堆 + +- 堆结构的heapInsert与heapify操作 + +1、heapInsert + +思路:例如我们要构建一个大根堆,我们把所有的数依次添加到一个数组(下标从0开始)中去,每次添加一个数的时候,要去用找父亲节点的公式parent = (i-1) / 2找到父节点区比较,如果比父节点大就和父节点交换向上移动,移动后再用自己当前位置和父亲节点比较...,小于等于父节点不做处理。这样用户每加一个数,我们都能保证该结构是大根堆,对应代码的push方法 + +> 我们的调整代价实际上就是这颗树的高度层数,logN + +2、heapify + +> 原堆结构,删除最大值,继续调整维持成大根堆 + +思路:我们删除了最大值,也就是arr[0]位置,之后我们把堆最末尾的位置调整到arr[0]位置,堆大小减一。让现在arr[0]位置的数找左右孩子比较...,进行hearify操作,让其沉下去。沉到合适的位置之后,仍然是大根堆。对应代码的pop方法 + +> heapify的下沉操作,仍然是树的高度,logN。堆结构很重要 + +```Go +package main + +import ( + "errors" +) + +type Heap interface { + IsEmpty() bool + IsFull() bool + Push(value int) error + Pop() int +} + +func assertListImplementation() { + var _ Heap = (*MaxHeap)(nil) +} + +type MaxHeap struct { + // 大根堆的底层数组结构 + heap []int + // 分配给堆的空间限制 + limit int + // 表示目前这个堆收集了多少个数,即堆大小。也表示添加的下一个数应该放在哪个位置 + heapSize int +} + +// NewMaxHeap 初始化一个大根堆结构 +func NewMaxHeap(limit int) *MaxHeap { + maxHeap := &MaxHeap{ + heap: make([]int, 0), + limit: limit, + heapSize: 0, + } + return maxHeap +} + +func (h *MaxHeap) IsEmpty() bool { + return len(h.heap) == 0 +} + +func (h *MaxHeap) IsFull() bool { + return h.heapSize == h.limit +} + +func (h *MaxHeap) Push(value int) error { + if h.heapSize == h.limit { + return errors.New("heap is full") + } + + h.heap[h.heapSize] = value + // heapSize的位置保存当前value + heapInsert(h.heap, h.heapSize) + h.heapSize++ + return nil +} + +// Pop 返回堆中的最大值,并且在大根堆中,把最大值删掉。弹出后依然保持大根堆的结构 +func (h *MaxHeap) Pop() int { + tmp := h.heap[0] + h.heapSize-- + swap(h.heap, 0, h.heapSize) + heapify(h.heap, 0, h.heapSize) + return tmp +} + +// 往堆上添加数,需要从当前位置找父节点比较 +func heapInsert(arr []int, index int) { + for arr[index] > arr[(index-1)/2] { + swap(arr, index, (index-1)/2) + index = (index - 1) / 2 + } +} + +// 从index位置,不断的与左右孩子比较,下沉。下沉终止条件为:1. 左右孩子都不大于当前值 2. 没有左右孩子了 +func heapify(arr []int, index int, heapSize int) { + // 左孩子的位置 + left := index*2 + 1 + // 左孩子越界,右孩子一定越界。退出循环的条件是:2. 没有左右孩子了 + for left < heapSize { + var largestIdx int + rigth := left + 1 + // 存在右孩子,且右孩子的值比左孩子大,选择右孩子的位置 + if rigth < heapSize && arr[rigth] > arr[left] { + largestIdx = rigth + } else { + largestIdx = left + } + + // 1. 左右孩子的最大值都不大于当前值,终止寻找。无需继续下沉 + if arr[largestIdx] <= arr[index] { + break + } + // 左右孩子的最大值大于当前值 + swap(arr, largestIdx, index) + // 当前位置移动到交换后的位置,继续寻找 + index = largestIdx + // 移动后左孩子理论上的位置,下一次循环判断越界情况 + left = index*2 + 1 + } +} + +// swap 交换数组中的两个位置的数 +func swap(arr []int, i, j int) { + tmp := arr[i] + arr[i] = arr[j] + arr[j] = tmp +} +``` + +### 1.1.5 堆排序 + +1. 对于用户给的所有数据,我们先让其构建成为大根堆 +2. 对于0到N-1位置的数,我们依次让N-1位置的数和0位置的数(全局最大值)交换,此时全局最大值来到了数组最大位置,堆大小减一,再heapify调整成大根堆。再用N-2位置的数和调整后的0位置的数交换,相同操作。直至0位置和0位置交换。每次heapify为logN,交换调整了N次 +3. 所以堆排序的时间复杂度为O(NlogN) +4. 堆排序额为空间复杂度为O(1),且不存在递归行为 + +```Go +package main + +// HeapSort 堆排序额外空间复杂度O(1) +func HeapSort(arr []int) { + if len(arr) < 2 { + return + } + + // 原始版本, 调整arr满足大根堆结构。O(N*logN) + //for i := 0; i < len(arr); i++ { // O(N) + // heapInsert(arr, i) // O(logN) + //} + + // 优化版本:heapInsert改为heapify。从末尾开始看是否需要heapify=》O(N)复杂度。 + // 但是这只是优化了原有都是构建堆(O(NlogN)),最终的堆排序仍然是O(NlogN)。比原始版本降低了常数项 + for i := len(arr) - 1; i >= 0; i-- { + heapify(arr, i, len(arr)) + } + + // 实例化一个大根堆,此时arr已经是调整后满足大根堆结构的arr + mh := MaxHeap{ + heap: arr, + limit: len(arr), + heapSize: len(arr), + } + + mh.heapSize -- + swap(arr, 0, mh.heapSize) + // O(N*logN) + for mh.heapSize > 0 { // O(N) + heapify(arr, 0, mh.heapSize) // O(logN) + mh.heapSize-- + swap(arr, 0, mh.heapSize) // O(1) + } + +} +``` + +> 关于上述heapInsert改为heapIfy的优化: + +在我们从0到N-1进行heapInsert的时候,是O(NlogN),很容易理解。当我们从N-1到0上依次heapify的时候,整体来看,整棵树的根节点的heapify层数N/2,第二层为N/4且有两个节点。那么实质是N个不同的层数相加: + +```math +T(N) = (\frac{N}{2} * 1) + (\frac{N}{4} * 2) + (\frac{N}{8} * 3) + (\frac{N}{16} * 4) + ... + +=> + +2T(N) = (\frac{N}{2} * 2) + (\frac{N}{2} * 2) + (\frac{N}{4} * 3) + (\frac{N}{8} * 4) + ... + +=> + +T(N) = N + \frac{N}{2} + \frac{N}{4} + \frac{N}{8} + ... + +=> O(N) + +``` + +**同理,可以按同样方式实现一个小根堆** + +**在有些语言中,已经实现了堆,例如Java的优先级队列java.util.PriorityQueue,Golang中的container/heap** + +### 1.1.6 语言、系统提供的堆和手写堆的选择 + +#### 1.1.6.1 系统堆和手写堆选择 + +> 使用系统提供的堆:如果我们只是要依次拿最大值,那么做成大根堆,如果我们要最小值我们把堆结构做成小根堆。就是简单的我们添加值,拿值,我们就选择系统提供的堆 + +> 选择手写堆:如果已经放到系统堆中的元素,加入我们根据需求会在放入堆之后要改动这些元素的值,系统堆并不保证弹出来的东西是正确的,这个时候需要我们手动写一个我们自定义的堆。虽然存在那种排好堆改某些元素让其重新有序的堆结构,但是实质上它是重新扫每个元素去heapinsert,代价太高。手动改写堆的例子例如Dijkstra算法就存在改写堆的优化 + +## 1.2 比较器 + +1、比较器的实质就是重载比较运算符 + +2、比较器可以很好的应用在特殊标准的排序上 + +3、比较器可以很好的应用在根据特殊标准排序的结构上 + +> 任何有序结构,我们可以传入我们的比较器,自定义我们自己的排序规则,不传它会按自己默认的规则排序 + +### 1.2.1 Golang中自定义比较行为 + +> 在Golang中如果需要自定义比较规则,只需要实现sort/srot.go中的Interface接口的Len、Less、Swap三个方法即可 + +```Go +package main + +import ( + "fmt" + "sort" +) + +// Comparator +// negative , if a < b +// zero , if a == b +// positive , if a > b +type Comparator func(a, b interface{}) int + +// 定义可排序的结构 +type sortable struct { + values []interface{} + // 该结构携带一个自定义的排序策略 + comparator Comparator +} + +// Sort 使用Go原生的排序进行包装,该排序在数据规模大的时候使用快排,数据规模小的时候使用插入排序 +func Sort(values []interface{}, comparator Comparator) { + sort.Sort(sortable{values, comparator}) +} + +func (s sortable) Len() int { + return len(s.values) +} + +func (s sortable) Swap(i, j int) { + s.values[i], s.values[j] = s.values[j], s.values[i] +} + +func (s sortable) Less(i, j int) bool { + return s.comparator(s.values[i], s.values[j]) < 0 +} + +// IntComparator 是自定义的整形排序策略,可以实现其他自定义排序策略 +func IntComparator(a, b interface{}) int { + aAsserted := a.(int) + bAsserted := b.(int) + switch { + case aAsserted > bAsserted: + return 1 + case aAsserted < bAsserted: + return -1 + default: + return 0 + } +} + +func main() { + tests := [][]interface{}{ + {1, 1, 0}, + {1, 2, -1}, + {2, 1, 1}, + {11, 22, -1}, + {0, 0, 0}, + {1, 0, 1}, + {0, 1, -1}, + } + for _, test := range tests { + Sort(test, IntComparator) + fmt.Println(test) + } +} +``` diff --git "a/04-\346\257\224\350\276\203\345\231\250\343\200\201\345\240\206.md" "b/04-\346\257\224\350\276\203\345\231\250\343\200\201\345\240\206.md" deleted file mode 100644 index ddc37d1..0000000 --- "a/04-\346\257\224\350\276\203\345\231\250\343\200\201\345\240\206.md" +++ /dev/null @@ -1,1151 +0,0 @@ -[TOC] -# 1 比较器与堆 - -## 1.1 堆结构 - -### 1.1.1 完全二叉树结构 - -> 完全二叉树结构:要么本层是满的,要么先满左边的,以下都是完全二叉树 - -1. - -``` -graph TD -A-->B -A-->C -``` - -2. - -``` -graph TD -A-->B -A-->C -B-->D -B-->E -C-->F -``` - - -### 1.1.2 数组实现堆 - -- 堆结构就是用数组实现的完全二叉树结构 - - -> 用数组实现完全二叉树结构,从数组下标0开始,当成依次补齐二叉树结构的数据 - -``` -graph TD -0--> 1 -0--> 2 -1--> 3 -1-->4 -2-->5 -2-->6 -``` - -某位置i的左孩子下标为: -```math -lchild = 2*i + 1 -``` -某位置i的右孩子的下标为: -```math -rchild = 2*i + 2 -``` -某位置i的父节点位置为: -```math -parent = (i-1) / 2 -``` - -> 当我们不使用数组的0下标,从1位置开始构建完全二叉树时,方便使用位操作: - -某位置i的左孩子下标为: -```math -lchild = 2*i <==> i << 1 -``` -某位置i的右孩子的下标为: -```math -rchild = 2*i + 1 <==> (i << 1) | 1 -``` -某位置i的父节点位置为: -```math -parent = i / 2 <==> i >> 1 -``` -### 1.1.3 大根堆与小根堆 - -- 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆 - -- 完全二叉树中如果每颗子树的最小值都在顶部就是小根堆 - -==我们认为堆就是大根堆或者小根堆,既不是大根堆也不是小根堆的完全二叉树只是完全二叉树,不能称之为堆== - - - -### 1.1.4 构建堆 - -- 堆结构的heapInsert与heapify操作 - -heapInsert - -思路:例如我们要构建一个大根堆,我们把所有的数依次添加到一个数组(下标从0开始)中去,每次添加一个数的时候,要去用找父亲节点的公式parent = (i-1) / 2找到父节点区比较,如果比父节点大就和父节点交换向上移动,移动后再用自己当前位置和父亲节点比较...,小于等于父节点不做处理。这样用户每加一个数,我们都能保证该结构是大根堆,对应代码的push方法 - -> 我们的调整代价实际上就是这颗树的高度层数,logN - -heapify - -> 原堆结构,删除最大值,继续调整维持成大根堆 - -思路:我们删除了最大值,也就是arr[0]位置,之后我们把堆最末尾的位置调整到arr[0]位置,堆大小减一。让现在arr[0]位置的数找左右孩子比较...,进行hearify操作,让其沉下去。沉到合适的位置之后,仍然是大根堆。对应代码的pop方法 - -> heapify的下沉操作,仍然是树的高度,logN - -> 堆结构很重要很重要 - -```Java -package class04; - -public class Code02_Heap01 { - - public static class MyMaxHeap { - // 我们的大根堆 - private int[] heap; - private final int limit; - // 表示目前这个堆收集了多少个数,也表示添加的下一个数应该放在哪个位置 - private int heapSize; - - public MyMaxHeap(int limit) { - heap = new int[limit]; - this.limit = limit; - heapSize = 0; - } - - public boolean isEmpty() { - return heapSize == 0; - } - - public boolean isFull() { - return heapSize == limit; - } - - // 每加入一个数,需要动态维持堆结构 - public void push(int value) { - if (heapSize == limit) { - throw new RuntimeException("heap is full"); - } - heap[heapSize] = value; - // value heapSize - heapInsert(heap, heapSize++); - } - - // 用户此时,让你返回最大值,并且在大根堆中,把最大值删掉 - // 剩下的数,依然保持大根堆组织 - public int pop() { - int ans = heap[0]; - swap(heap, 0, --heapSize); - heapify(heap, 0, heapSize); - return ans; - } - - // 往堆上添加数,需要用当前位置找父节点比较 - private void heapInsert(int[] arr, int index) { - // arr[index] - // arr[index] 不比 arr[index父]大了 , 停 - // index = 0时也停 - while (arr[index] > arr[(index - 1) / 2]) { - swap(arr, index, (index - 1) / 2); - index = (index - 1) / 2; - } - } - - // 从index位置,往下看,不断的下沉, - // 停的条件:我的孩子都不再比我大;已经没孩子了 - private void heapify(int[] arr, int index, int heapSize) { - int left = index * 2 + 1; - // 左孩子没越界,如果左孩子越界有孩子一定也越界 - while (left < heapSize) { - // 左右两个孩子中,谁大,谁把自己的下标给largest - // 什么请款下选择右 -> (1) 有右孩子 && (2)右孩子的值比左孩子大才行 - // 否则,左 - int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; - // 左右孩子中最大值,和当前值比较,谁大谁把下标给largest(当前,左,右的最大值下标) - largest = arr[largest] > arr[index] ? largest : index; - // index位置上的数比左右孩子的数都大,已经无需下沉 - if (largest == index) { - break; - } - // 交换后,继续找左右孩子进行比较,周而复始 - swap(arr, largest, index); - index = largest; - left = index * 2 + 1; - } - } - - private void swap(int[] arr, int i, int j) { - int tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } - - } - - // 暴力,O(N)复杂度实现的大根堆。用来做对数器 - public static class RightMaxHeap { - private int[] arr; - private final int limit; - private int size; - - public RightMaxHeap(int limit) { - arr = new int[limit]; - this.limit = limit; - size = 0; - } - - public boolean isEmpty() { - return size == 0; - } - - public boolean isFull() { - return size == limit; - } - - public void push(int value) { - if (size == limit) { - throw new RuntimeException("heap is full"); - } - arr[size++] = value; - } - - public int pop() { - int maxIndex = 0; - for (int i = 1; i < size; i++) { - if (arr[i] > arr[maxIndex]) { - maxIndex = i; - } - } - int ans = arr[maxIndex]; - arr[maxIndex] = arr[--size]; - return ans; - } - - } - - public static void main(String[] args) { - int value = 1000; - int limit = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - int curLimit = (int) (Math.random() * limit) + 1; - MyMaxHeap my = new MyMaxHeap(curLimit); - RightMaxHeap test = new RightMaxHeap(curLimit); - int curOpTimes = (int) (Math.random() * limit); - for (int j = 0; j < curOpTimes; j++) { - if (my.isEmpty() != test.isEmpty()) { - System.out.println("Oops!"); - } - if (my.isFull() != test.isFull()) { - System.out.println("Oops!"); - } - if (my.isEmpty()) { - int curValue = (int) (Math.random() * value); - my.push(curValue); - test.push(curValue); - } else if (my.isFull()) { - if (my.pop() != test.pop()) { - System.out.println("Oops!"); - } - } else { - if (Math.random() < 0.5) { - int curValue = (int) (Math.random() * value); - my.push(curValue); - test.push(curValue); - } else { - if (my.pop() != test.pop()) { - System.out.println("Oops!"); - } - } - } - } - } - System.out.println("finish!"); - - } - -} -``` - - -### 1.1.5 堆排序 - -1. 对于用户给的所有数据,我们先让其构建成为大根堆 -2. 对于0到N-1位置的数,我们依次让N-1位置的数和0位置的数(全局最大值)交换,此时全局最大值来到了数组最大位置,堆大小减一,再heapify调整成大根堆。再用N-2位置的数和调整后的0位置的数交换,相同操作。直至0位置和0位置交换。每次heapify为logN,交换调整了N次 -3. 所以堆排序的时间复杂度为O(NlogN) -4. 堆排序额为空间复杂度为O(1),且不存在递归行为 - - -```Java -package class04; - -import java.util.Arrays; -import java.util.PriorityQueue; - -public class Code04_HeapSort { - - // 堆排序额外空间复杂度O(1) - public static void heapSort(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - // O(N*logN),原始版本 -// for (int i = 0; i < arr.length; i++) { // O(N) -// heapInsert(arr, i); // O(logN) -// } - - // 优化版本,heapInsert改为heapify。从末尾开始看是否需要heapify=》O(N)复杂度。 - // 但是这只是优化了原有都是构建堆(O(NlogN)),最终的堆排序仍然是O(NlogN) - for (int i = arr.length - 1; i >= 0; i--) { - heapify(arr, i, arr.length); - } - int heapSize = arr.length; - swap(arr, 0, --heapSize); - // O(N*logN) - while (heapSize > 0) { // O(N) - heapify(arr, 0, heapSize); // O(logN) - swap(arr, 0, --heapSize); // O(1) - } - } - - // arr[index]刚来的数,往上 - public static void heapInsert(int[] arr, int index) { - while (arr[index] > arr[(index - 1) / 2]) { - swap(arr, index, (index - 1) / 2); - index = (index - 1) / 2; - } - } - - // arr[index]位置的数,能否往下移动 - public static void heapify(int[] arr, int index, int heapSize) { - // 左孩子的下标 - int left = index * 2 + 1; - // 下方还有孩子的时候 - while (left < heapSize) { - // 两个孩子中,谁的值大,把下标给largest - // 1)只有左孩子,left -> largest - // 2) 同时有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest - // 3) 同时有左孩子和右孩子并且右孩子的值> 左孩子的值, right -> largest - int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; - // 父和较大的孩子之间,谁的值大,把下标给largest - largest = arr[largest] > arr[index] ? largest : index; - if (largest == index) { - break; - } - swap(arr, largest, index); - index = largest; - left = index * 2 + 1; - } - } - - public static void swap(int[] arr, int i, int j) { - int tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } - - // for test - public static void comparator(int[] arr) { - Arrays.sort(arr); - } - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - return arr; - } - - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; - } - return res; - } - - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; - } - - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - // for test - public static void main(String[] args) { - - // 默认小根堆 - PriorityQueue heap = new PriorityQueue<>(); - heap.add(6); - heap.add(8); - heap.add(0); - heap.add(2); - heap.add(9); - heap.add(1); - - while (!heap.isEmpty()) { - System.out.println(heap.poll()); - } - - int testTime = 500000; - int maxSize = 100; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - heapSort(arr1); - comparator(arr2); - if (!isEqual(arr1, arr2)) { - succeed = false; - break; - } - } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - - int[] arr = generateRandomArray(maxSize, maxValue); - printArray(arr); - heapSort(arr); - printArray(arr); - } - -} - -``` - -> 关于上述heapInsert改为heapIfy的优化: - -在我们从0到N-1进行heapInsert的时候,是O(NlogN)不做解释,当我们从N-1到0上依次heapify的时候,整体来看,整棵树的跟节点的heapify层数N/2,第二层为N/4且有两个节点。那么实质是N个不同的层数相加: - -```math -T(N) = (\frac{N}{2} * 1) + (\frac{N}{4} * 2) + (\frac{N}{8} * 3) + (\frac{N}{16} * 4) + ... - -=> - -2T(N) = (\frac{N}{2} * 2) + (\frac{N}{2} * 2) + (\frac{N}{4} * 3) + (\frac{N}{8} * 4) + ... - -=> - -T(N) = N + \frac{N}{2} + \frac{N}{4} + \frac{N}{8} + ... - -=> O(N) - -``` - -### 1.1.6 语言、系统提供的堆和手写堆的选择 - -#### 1.1.6.1 系统实现的堆 - -> 系统实现的堆实质上就是优先级队列,虽然名称叫优先级队列,底层就是堆实现的。默认是小根堆,我们可以自定义比较器把它改为大根堆 - -```Java -package class04; - -import java.util.Comparator; -import java.util.PriorityQueue; - - -public class Test { - - // 负数,o1 放在上面的情况 - public static class MyComp implements Comparator{ - - @Override - public int compare(Integer o1, Integer o2) { - return o2 - o1; - } - - } - - - - public static void main(String[] args) { - System.out.println("hello"); - // 大根堆 - PriorityQueue heap = new PriorityQueue<>(new MyComp()); - - heap.add(5); - heap.add(7); - heap.add(3); - heap.add(0); - heap.add(2); - heap.add(5); - - while(!heap.isEmpty()) { - System.out.println(heap.poll()); - } - - } -} -``` - -堆的相关面试题: - -题目一:已知一个几乎有序的数组。几乎有序是指,如果把数组排好序的话,每个元素移动的距离一定不超过k,并且k相对于数组长度来说是比较小的。请选择一个合适的排序策略,对这个数组进行排序 - -> 思路:例如给定一个数组,k=5,那么我们从0开始,前K+1个数也就是0到5位置的数放到小根堆,排序之后把最小的放到0位置,接下来把6位置放小根堆(此时小根堆里面有0到6位置的数),由于0位置的数有距离限制只能从0到5上选择,所以此时弹出最小值放到1位置,此时1位置被固定... - -```Java -package class04; - -import java.util.Arrays; -import java.util.PriorityQueue; - -public class Code05_SortArrayDistanceLessK { - - public static void sortedArrDistanceLessK(int[] arr, int k) { - if (k == 0) { - return; - } - // 默认小根堆 - PriorityQueue heap = new PriorityQueue<>(); - int index = 0; - // 0...K-1 - for (; index <= Math.min(arr.length - 1, k - 1); index++) { - heap.add(arr[index]); - } - int i = 0; - for (; index < arr.length; i++, index++) { - heap.add(arr[index]); - arr[i] = heap.poll(); - } - while (!heap.isEmpty()) { - arr[i++] = heap.poll(); - } - } - - // for test - public static void comparator(int[] arr, int k) { - Arrays.sort(arr); - } - - // for test - public static int[] randomArrayNoMoveMoreK(int maxSize, int maxValue, int K) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); - } - // 先排个序 - Arrays.sort(arr); - // 然后开始随意交换,但是保证每个数距离不超过K - // swap[i] == true, 表示i位置已经参与过交换 - // swap[i] == false, 表示i位置没有参与过交换 - boolean[] isSwap = new boolean[arr.length]; - for (int i = 0; i < arr.length; i++) { - int j = Math.min(i + (int) (Math.random() * (K + 1)), arr.length - 1); - if (!isSwap[i] && !isSwap[j]) { - isSwap[i] = true; - isSwap[j] = true; - int tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } - } - return arr; - } - - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; - } - return res; - } - - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; - } - - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - // for test - public static void main(String[] args) { - System.out.println("test begin"); - int testTime = 500000; - int maxSize = 100; - int maxValue = 100; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int k = (int) (Math.random() * maxSize) + 1; - int[] arr = randomArrayNoMoveMoreK(maxSize, maxValue, k); - int[] arr1 = copyArray(arr); - int[] arr2 = copyArray(arr); - sortedArrDistanceLessK(arr1, k); - comparator(arr2, k); - if (!isEqual(arr1, arr2)) { - succeed = false; - System.out.println("K : " + k); - printArray(arr); - printArray(arr1); - printArray(arr2); - break; - } - } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - } - -} -``` - -> 时间复杂度O(NlogK) - -#### 1.1.6.2 系统堆和手写堆选择 - -> 使用系统提供的堆:如果我们只是要依次拿最大值,那么做成大根堆,如果我们要最小值我们把堆结构做成小根堆。就是简单的我们添加值,拿值,我们就选择系统提供的堆 - -> 选择手写堆:如果已经放到系统堆中的元素,加入我们根据需求会在放入堆之后要改动这些元素的值,系统堆并不保证弹出来的东西是正确的,这个时候需要我们手动写一个我们自定义的堆。虽然存在那种排好堆改某些元素让其重新有序的堆结构,但是实质上它是重新扫每个元素去heapinsert,代价太高。手动改写堆的例子例如Dijkstra算法就存在改写堆的优化 - -```Java -package class04; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.PriorityQueue; - -public class Code03_Heap02 { - - // 堆 - public static class MyHeap { - // 堆结构,数组实现 - private ArrayList heap; - // 任意一个元素,我们记录它在我们堆上的位置信息(反向表),此时我们找到我们要改的元素的位置就O(1) - private HashMap indexMap; - // 堆大小 - private int heapSize; - // 比较规则 - private Comparator comparator; - - // 构造 - public MyHeap(Comparator com) { - heap = new ArrayList<>(); - indexMap = new HashMap<>(); - heapSize = 0; - comparator = com; - } - - public boolean isEmpty() { - return heapSize == 0; - } - - public int size() { - return heapSize; - } - - public boolean contains(T key) { - return indexMap.containsKey(key); - } - - public void push(T value) { - heap.add(value); - // 由于依次添加元素,添加进来的元素位置就是heapSize - indexMap.put(value, heapSize); - heapInsert(heapSize++); - } - - // 弹出0号位置的元素,要同步堆和字典的操作 - public T pop() { - T ans = heap.get(0); - int end = heapSize - 1; - swap(0, end); - heap.remove(end); - indexMap.remove(ans); - heapify(0, --heapSize); - return ans; - } - - - // 用来满足自定义的需求,用户要改某个元素的值,我们需要改过之后继续维持堆结构 - public void resign(T value) { - int valueIndex = indexMap.get(value); - // 改变值之后,我们不确定是值变大了还是变小了,即不确定是需要heapInsert还是heapify,但是两个操作只会命中一个 - heapInsert(valueIndex); - heapify(valueIndex, heapSize); - } - - // heapInsert时,需要用我们自己的比较器进行比较 - private void heapInsert(int index) { - while (comparator.compare(heap.get(index), heap.get((index - 1) / 2)) < 0) { - swap(index, (index - 1) / 2); - index = (index - 1) / 2; - } - } - - private void heapify(int index, int heapSize) { - int left = index * 2 + 1; - while (left < heapSize) { - int largest = left + 1 < heapSize && (comparator.compare(heap.get(left + 1), heap.get(left)) < 0) - ? left + 1 - : left; - largest = comparator.compare(heap.get(largest), heap.get(index)) < 0 ? largest : index; - if (largest == index) { - break; - } - swap(largest, index); - index = largest; - left = index * 2 + 1; - } - } - - // 每次交换,不经要交换堆中两个位置的元素,在我们的字典中也要要换位置 - private void swap(int i, int j) { - T o1 = heap.get(i); - T o2 = heap.get(j); - heap.set(i, o2); - heap.set(j, o1); - indexMap.put(o1, j); - indexMap.put(o2, i); - } - - } - - public static class Student { - public int classNo; - public int age; - public int id; - - public Student(int c, int a, int i) { - classNo = c; - age = a; - id = i; - } - - } - - public static class StudentComparator implements Comparator { - - @Override - public int compare(Student o1, Student o2) { - return o1.age - o2.age; - } - - } - - public static void main(String[] args) { - Student s1 = null; - Student s2 = null; - Student s3 = null; - Student s4 = null; - Student s5 = null; - Student s6 = null; - - s1 = new Student(2, 50, 11111); - s2 = new Student(1, 60, 22222); - s3 = new Student(6, 10, 33333); - s4 = new Student(3, 20, 44444); - s5 = new Student(7, 72, 55555); - s6 = new Student(1, 14, 66666); - - PriorityQueue heap = new PriorityQueue<>(new StudentComparator()); - heap.add(s1); - heap.add(s2); - heap.add(s3); - heap.add(s4); - heap.add(s5); - heap.add(s6); - while (!heap.isEmpty()) { - Student cur = heap.poll(); - System.out.println(cur.classNo + "," + cur.age + "," + cur.id); - } - - System.out.println("==============="); - - MyHeap myHeap = new MyHeap<>(new StudentComparator()); - myHeap.push(s1); - myHeap.push(s2); - myHeap.push(s3); - myHeap.push(s4); - myHeap.push(s5); - myHeap.push(s6); - while (!myHeap.isEmpty()) { - Student cur = myHeap.pop(); - System.out.println(cur.classNo + "," + cur.age + "," + cur.id); - } - - System.out.println("==============="); - - s1 = new Student(2, 50, 11111); - s2 = new Student(1, 60, 22222); - s3 = new Student(6, 10, 33333); - s4 = new Student(3, 20, 44444); - s5 = new Student(7, 72, 55555); - s6 = new Student(1, 14, 66666); - - heap = new PriorityQueue<>(new StudentComparator()); - - heap.add(s1); - heap.add(s2); - heap.add(s3); - heap.add(s4); - heap.add(s5); - heap.add(s6); - - s2.age = 6; - s4.age = 12; - s5.age = 10; - s6.age = 84; - - while (!heap.isEmpty()) { - Student cur = heap.poll(); - System.out.println(cur.classNo + "," + cur.age + "," + cur.id); - } - - System.out.println("==============="); - - s1 = new Student(2, 50, 11111); - s2 = new Student(1, 60, 22222); - s3 = new Student(6, 10, 33333); - s4 = new Student(3, 20, 44444); - s5 = new Student(7, 72, 55555); - s6 = new Student(1, 14, 66666); - - myHeap = new MyHeap<>(new StudentComparator()); - - myHeap.push(s1); - myHeap.push(s2); - myHeap.push(s3); - myHeap.push(s4); - myHeap.push(s5); - myHeap.push(s6); - - s2.age = 6; - myHeap.resign(s2); - s4.age = 12; - myHeap.resign(s4); - s5.age = 10; - myHeap.resign(s5); - s6.age = 84; - myHeap.resign(s6); - - while (!myHeap.isEmpty()) { - Student cur = myHeap.pop(); - System.out.println(cur.classNo + "," + cur.age + "," + cur.id); - } - - // 对数器 - System.out.println("test begin"); - int maxValue = 100000; - int pushTime = 1000000; - int resignTime = 100; - MyHeap test = new MyHeap<>(new StudentComparator()); - ArrayList list = new ArrayList<>(); - for(int i = 0 ; i < pushTime; i++) { - Student cur = new Student(1,(int) (Math.random() * maxValue), 1000); - list.add(cur); - test.push(cur); - } - for(int i = 0 ; i < resignTime; i++) { - int index = (int)(Math.random() * pushTime); - list.get(index).age = (int) (Math.random() * maxValue); - test.resign(list.get(index)); - } - int preAge = Integer.MIN_VALUE; - while(test.isEmpty()) { - Student cur = test.pop(); - if(cur.age < preAge) { - System.out.println("Oops!"); - } - preAge = cur.age; - } - System.out.println("test finish"); - } - -} -``` - -## 1.2 比较器 - -1、比较器的实质就是重载比较运算符 - -2、比较器可以很好的应用在特殊标准的排序上 - -3、比较器可以很好的应用在根据特殊标准排序的结构上 - -> 任何有序结构,我们可以传入我们的比较器,自定义我们自己的排序规则,不传它会按自己默认的规则排序 - -4、写代码变得异常容易,还用于泛型编程 - -> 比较规则中o1,o2,比较器返回负数表示o1要排在前面,返回正数表示o1要排在后面,返回0表示o1和o1相等无需排序。在java中自定义的比较器(MyComparator)实现Comparator接口,实现该接口中的compare方法,自定义我们的比较规则。 - - -> 使用示例:Arrays.sort(student, new MyComparator()) - -```Java -package class04; - -import java.util.Arrays; -import java.util.Comparator; -import java.util.PriorityQueue; -import java.util.TreeSet; - -public class Code01_Comparator { - - // 自定义我们的排序对象 - public static class Student { - public String name; - public int id; - public int age; - - public Student(String name, int id, int age) { - this.name = name; - this.id = id; - this.age = age; - } - } - - public static class IdAscendingComparator - - implements Comparator { - - // 返回负数的时候,第一个参数排在前面 - // 返回正数的时候,第二个参数排在前面 - // 返回0的时候,谁在前面无所谓 - @Override - public int compare(Student o1, Student o2) { - return o1.id - o2.id; - } - - } - - public static class IdDescendingComparator implements Comparator { - - @Override - public int compare(Student o1, Student o2) { - return o2.id - o1.id; - } - - } - - public static class AgeAscendingComparator implements Comparator { - - @Override - public int compare(Student o1, Student o2) { - return o1.age - o2.age; - } - - } - - public static class AgeDescendingComparator implements Comparator { - - @Override - public int compare(Student o1, Student o2) { - return o2.age - o1.age; - } - - } - - - public static class AgeShengIdSheng implements Comparator { - - @Override - public int compare(Student o1, Student o2) { - return o1.age != o2.age ? (o1.age - o2.age) - : (o1.id - o2.id); - } - - } - - - // 先按照id排序,id小的,放前面; - // id一样,age大的,前面; - public static class IdInAgeDe implements Comparator { - - @Override - public int compare(Student o1, Student o2) { - return o1.id != o2.id ? o1.id - o2.id : ( o2.age - o1.age ); - } - - } - - - public static void printStudents(Student[] students) { - for (Student student : students) { - System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age); - } - } - - public static void printArray(Integer[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - public static class MyComp implements Comparator { - - @Override - public int compare(Integer o1, Integer o2) { - return o2 - o1; - } - - } - - - public static class AComp implements Comparator{ - - // 如果返回负数,认为第一个参数应该拍在前面 - // 如果返回正数,认为第二个参数应该拍在前面 - // 如果返回0,认为谁放前面都行 - @Override - public int compare(Integer arg0, Integer arg1) { - - return arg1 - arg0; - - // return 0; - } - - } - - - public static void main(String[] args) { - - Integer[] arr = {5,4,3,2,7,9,1,0}; - - Arrays.sort(arr, new AComp()); - - for(int i = 0 ;i < arr.length;i++) { - System.out.println(arr[i]); - } - - System.out.println("==========================="); - - Student student1 = new Student("A", 2, 20); - Student student2 = new Student("B", 3, 21); - Student student3 = new Student("C", 1, 22); - - Student[] students = new Student[] { student1, student2, student3 }; - System.out.println("第一条打印"); - - - Arrays.sort(students, new IdAscendingComparator()); - - - printStudents(students); - System.out.println("==========================="); - - - - Arrays.sort(students, new IdDescendingComparator()); - printStudents(students); - System.out.println("==========================="); - - Arrays.sort(students, new AgeAscendingComparator()); - printStudents(students); - System.out.println("==========================="); -//// -//// Arrays.sort(students, new AgeDescendingComparator()); -//// printStudents(students); -//// System.out.println("==========================="); -// -// Arrays.sort(students, new AgeShengIdSheng()); -// printStudents(students); -// -// System.out.println("==========================="); -// System.out.println("==========================="); -// System.out.println("==========================="); -// -// PriorityQueue maxHeapBasedAge = new PriorityQueue<>(new AgeDescendingComparator()); -// maxHeapBasedAge.add(student1); -// maxHeapBasedAge.add(student2); -// maxHeapBasedAge.add(student3); -// while (!maxHeapBasedAge.isEmpty()) { -// Student student = maxHeapBasedAge.poll(); -// System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age); -// } -// System.out.println("==========================="); - - PriorityQueue minHeapBasedId - = new PriorityQueue<>(new AgeAscendingComparator()); - minHeapBasedId.add(student1); - minHeapBasedId.add(student2); - minHeapBasedId.add(student3); - while (!minHeapBasedId.isEmpty()) { - Student student = minHeapBasedId.poll(); - System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age); - } - System.out.println("==========================="); - System.out.println("==========================="); - System.out.println("==========================="); - - TreeSet treeAgeDescending = new TreeSet<>(new AgeAscendingComparator()); - treeAgeDescending.add(student1); - treeAgeDescending.add(student2); - treeAgeDescending.add(student3); - - Student studentFirst = treeAgeDescending.first(); - System.out.println("Name : " + studentFirst.name + ", Id : " + studentFirst.id + ", Age : " + studentFirst.age); - - Student studentLast = treeAgeDescending.last(); - System.out.println("Name : " + studentLast.name + ", Id : " + studentLast.id + ", Age : " + studentLast.age); - System.out.println("==========================="); - - } - -} -``` diff --git "a/05-\345\211\215\347\274\200\346\240\221\343\200\201\346\241\266\346\216\222\345\272\217\343\200\201\346\216\222\345\272\217\346\200\273\347\273\223.md" "b/05-\345\211\215\347\274\200\346\240\221\343\200\201\346\241\266\346\216\222\345\272\217\343\200\201\346\216\222\345\272\217\346\200\273\347\273\223.md" index f0fa5e7..ff16718 100644 --- "a/05-\345\211\215\347\274\200\346\240\221\343\200\201\346\241\266\346\216\222\345\272\217\343\200\201\346\216\222\345\272\217\346\200\273\347\273\223.md" +++ "b/05-\345\211\215\347\274\200\346\240\221\343\200\201\346\241\266\346\216\222\345\272\217\343\200\201\346\216\222\345\272\217\346\200\273\347\273\223.md" @@ -3,359 +3,144 @@ ## 1.1 前缀树结构 -> 单个字符串中,字符从前到后的加到一颗多叉树上 - -> 字符放在路上,节点上有专属的数据项(常见的是pass和end值) - -> 所有样本都这样添加。如果没有路就新建,如果有路就复用 - -> 沿途节点的pass值增加1.每个字符串结束时来到的节点end值增加1 - -> 一个字符串数组中,所有字符串的字符数为N,整个数组加入前缀树种的代价是O(N) +- 单个字符串中,字符从前到后的加到一颗多叉树上 +- 字符放在路上,节点上有专属的数据项(常见的是pass和end值) +- 所有样本都这样添加。如果没有路就新建,如果有路就复用 +- 沿途节点的pass值增加1.每个字符串结束时来到的节点end值增加1 + 一个字符串数组中,所有字符串的字符数为N,整个数组加入前缀树种的代价是O(N) 功能一:构建好前缀树之后,我们查询某个字符串在不在前缀树中,某字符串在这颗前缀树中出现了几次都是特别方便的。例如找"ab"在前缀树中存在几次,可以先看有无走向a字符的路径(如果没有,直接不存在),再看走向b字符的路径,此时检查该节点的end标记的值,如果为0,则前缀树中不存在"ab"字符串,如果e>0则,e等于几则"ab"在前缀树种出现了几次 功能二:如果单单是功能一,那么哈希表也可以实现。现查询所有加入到前缀树的字符串,有多少个以"a"字符作为前缀,来到"a"的路径,查看p值大小,就是以"a"作为前缀的字符串数量 -```Java -package class05; - -import java.util.HashMap; - -// 该程序完全正确 -public class Code02_TrieTree { - - public static class Node1 { - // pass表示字符从该节点的路径通过 - public int pass; - // end表示该字符到此节点结束 - public int end; - public Node1[] nexts; - - public Node1() { - pass = 0; - end = 0; - // 每个节点下默认26条路,分别是a~z - // 0 a - // 1 b - // 2 c - // .. .. - // 25 z - // nexts[i] == null i方向的路不存在 - // nexts[i] != null i方向的路存在 - nexts = new Node1[26]; - } - } - - public static class Trie1 { - // 默认只留出头节点 - private Node1 root; - - public Trie1() { - root = new Node1(); - } - - // 往该前缀树中添加字符串 - public void insert(String word) { - if (word == null) { - return; - } - char[] str = word.toCharArray(); - // 初始引用指向头节点 - Node1 node = root; - // 头结点的pass首先++ - node.pass++; - // 路径的下标 - int path = 0; - for (int i = 0; i < str.length; i++) { // 从左往右遍历字符 - // 当前字符减去'a'的ascii码得到需要添加的下个节点下标 - path = str[i] - 'a'; // 由字符,对应成走向哪条路 - // 当前方向上没有建立节点,即一开始不存在这条路,新开辟 - if (node.nexts[path] == null) { - node.nexts[path] = new Node1(); - } - // 引用指向当前来到的节点 - node = node.nexts[path]; - // 当前节点的pass++ - node.pass++; - } - // 当新加的字符串所有字符处理结束,最后引用指向的当前节点就是该字符串的结尾节点,end++ - node.end++; - } +```Go +package main - // 删除该前缀树的某个字符串 - public void delete(String word) { - // 首先要查一下该字符串是否加入过 - if (search(word) != 0) { - // 沿途pass-- - char[] chs = word.toCharArray(); - Node1 node = root; - node.pass--; - int path = 0; - for (int i = 0; i < chs.length; i++) { - path = chs[i] - 'a'; - // 在寻找的过程中,pass为0,提前可以得知在本次删除之后,该节点以下的路径不再需要,可以直接删除。 - // 那么该节点之下下个方向的节点引用置为空(JVM垃圾回收,相当于该节点下的路径被删了) - if (--node.nexts[path].pass == 0) { - node.nexts[path] = null; - return; - } - node = node.nexts[path]; - } - // 最后end-- - node.end--; - } - } - // 在该前缀树中查找 - // word这个单词之前加入过几次 - public int search(String word) { - if (word == null) { - return 0; - } - char[] chs = word.toCharArray(); - Node1 node = root; - int index = 0; - for (int i = 0; i < chs.length; i++) { - index = chs[i] - 'a'; - // 寻找该字符串的路径中如果提前找不到path,就是未加入过,0次 - if (node.nexts[index] == null) { - return 0; - } - node = node.nexts[index]; - } - // 如果顺利把word字符串在前缀树中走完路径,那么此时的node对应的end值就是当前word在该前缀树中添加了几次 - return node.end; - } +// Node 前缀树的节点 +type Node struct { + Pass int + End int + Childes []*Node +} - // 所有加入的字符串中,有几个是以pre这个字符串作为前缀的 - public int prefixNumber(String pre) { - if (pre == null) { - return 0; - } - char[] chs = pre.toCharArray(); - Node1 node = root; - int index = 0; - for (int i = 0; i < chs.length; i++) { - index = chs[i] - 'a'; - // 走不到最后,就没有 - if (node.nexts[index] == null) { - return 0; - } - node = node.nexts[index]; - } - // 顺利走到最后,返回的pass就是有多少个字符串以当前pre为前缀的 - return node.pass; - } +func NewTrie() (root *Node) { + trie := &Node{ + Pass: 0, + End: 0, + // 默认保存26个英文字符a~z + // 0 a + // 1 b + // .. .. + // 25 z + // Childes[i] == nil 表示i方向的路径不存在 + Childes: make([]*Node, 26), } + return trie +} - - /** - * 实现方式二,针对各种字符串,路径不仅仅是a~z对应的26个,用HashMap表示ascii码值对应的node。 - **/ - public static class Node2 { - public int pass; - public int end; - public HashMap nexts; - - public Node2() { - pass = 0; - end = 0; - nexts = new HashMap<>(); - } +// Insert 往该前缀树中添加字符串 +func (root *Node) Insert(word string) { + if len(word) == 0 { + return } - - public static class Trie2 { - private Node2 root; - - public Trie2() { - root = new Node2(); - } - - public void insert(String word) { - if (word == null) { - return; - } - char[] chs = word.toCharArray(); - Node2 node = root; - node.pass++; - int index = 0; - for (int i = 0; i < chs.length; i++) { - index = (int) chs[i]; - if (!node.nexts.containsKey(index)) { - node.nexts.put(index, new Node2()); - } - node = node.nexts.get(index); - node.pass++; + // 字符串转字符数组,每个元素是字符的ascii码 + chs := []byte(word) + node := root + // 头结点的pass首先++ + node.Pass++ + // 路径的下标 + var path int + // 从左往右遍历字符 + for i := 0; i < len(chs); i++ { + // 当前字符减去'a'的ascii码得到需要添加的下个节点下标。即当前字符去往的路径 + path = int(chs[i] - 'a') + // 当前方向上没有建立节点,即一开始不存在这条路,新开辟 + if node.Childes[path] == nil { + node.Childes[path] = &Node{ + Pass: 0, + End: 0, + Childes: make([]*Node, 26), } - node.end++; - } - - public void delete(String word) { - if (search(word) != 0) { - char[] chs = word.toCharArray(); - Node2 node = root; - node.pass--; - int index = 0; - for (int i = 0; i < chs.length; i++) { - index = (int) chs[i]; - if (--node.nexts.get(index).pass == 0) { - node.nexts.remove(index); - return; - } - node = node.nexts.get(index); - } - node.end--; - } - } - - // word这个单词之前加入过几次 - public int search(String word) { - if (word == null) { - return 0; - } - char[] chs = word.toCharArray(); - Node2 node = root; - int index = 0; - for (int i = 0; i < chs.length; i++) { - index = (int) chs[i]; - if (!node.nexts.containsKey(index)) { - return 0; - } - node = node.nexts.get(index); - } - return node.end; - } - - // 所有加入的字符串中,有几个是以pre这个字符串作为前缀的 - public int prefixNumber(String pre) { - if (pre == null) { - return 0; - } - char[] chs = pre.toCharArray(); - Node2 node = root; - int index = 0; - for (int i = 0; i < chs.length; i++) { - index = (int) chs[i]; - if (!node.nexts.containsKey(index)) { - return 0; - } - node = node.nexts.get(index); - } - return node.pass; } + // 引用指向当前来到的节点 + node = node.Childes[path] + // 当前节点的pass++ + node.Pass++ } + // 当新加的字符串所有字符处理结束,最后引用指向的当前节点就是该字符串的结尾节点,end++ + node.End++ +} - /** - * 不用前缀树,纯暴力的组织,用来做对数器 - **/ - public static class Right { - - private HashMap box; - - public Right() { - box = new HashMap<>(); - } - - public void insert(String word) { - if (!box.containsKey(word)) { - box.put(word, 1); - } else { - box.put(word, box.get(word) + 1); - } - } - - public void delete(String word) { - if (box.containsKey(word)) { - if (box.get(word) == 1) { - box.remove(word); - } else { - box.put(word, box.get(word) - 1); - } - } - } - - public int search(String word) { - if (!box.containsKey(word)) { - return 0; - } else { - return box.get(word); - } - } - - public int prefixNumber(String pre) { - int count = 0; - for (String cur : box.keySet()) { - if (cur.startsWith(pre)) { - count += box.get(cur); - } - } - return count; - } +// Search 在该前缀树中查找word这个单词之前加入过几次 +func (root *Node) Search(word string) int { + if len(word) == 0 { + return 0 + } + chs := []byte(word) + node := root + index := 0 + for i := 0; i < len(chs); i++ { + index = int(chs[i] - 'a') + // 寻找该字符串的路径中如果提前找不到path,就是未加入过,0次 + if node.Childes[index] == nil { + return 0 + } + node = node.Childes[index] } + // 如果顺利把word字符串在前缀树中走完路径,那么此时的node对应的end值就是当前word在该前缀树中添加了几次 + return node.End +} - // for test - public static String generateRandomString(int strLen) { - char[] ans = new char[(int) (Math.random() * strLen) + 1]; - for (int i = 0; i < ans.length; i++) { - int value = (int) (Math.random() * 6); - ans[i] = (char) (97 + value); +// Delete 删除该前缀树的某个字符串 +func (root *Node) Delete(word string) { + // 首先要查一下该字符串是否加入过 + if root.Search(word) != 0 { + // 沿途pass-- + chs := []byte(word) + node := root + node.Pass-- + path := 0 + for i := 0; i < len(chs); i++ { + path = int(chs[i] - 'a') + // 在寻找的过程中,pass为0,提前可以得知在本次删除之后,该节点以下的路径不再需要,可以直接删除。 + // 那么该节点之下下个方向的节点引用置为空(JVM垃圾回收,相当于该节点下的路径被删了) + node.Childes[path].Pass-- + if node.Childes[path].Pass == 0 { + node.Childes[path] = nil + return + } + node = node.Childes[path] } - return String.valueOf(ans); + // 最后end-- + node.End-- } +} - // for test - public static String[] generateRandomStringArray(int arrLen, int strLen) { - String[] ans = new String[(int) (Math.random() * arrLen) + 1]; - for (int i = 0; i < ans.length; i++) { - ans[i] = generateRandomString(strLen); - } - return ans; +// PrefixNumber 所有加入的字符串中,有几个是以pre这个字符串作为前缀的 +func (root *Node) PrefixNumber(pre string) int { + if len(pre) == 0 { + return 0 } - public static void main(String[] args) { - int arrLen = 100; - int strLen = 20; - int testTimes = 100000; - for (int i = 0; i < testTimes; i++) { - String[] arr = generateRandomStringArray(arrLen, strLen); - Trie1 trie1 = new Trie1(); - Trie2 trie2 = new Trie2(); - Right right = new Right(); - for (int j = 0; j < arr.length; j++) { - double decide = Math.random(); - if (decide < 0.25) { - trie1.insert(arr[j]); - trie2.insert(arr[j]); - right.insert(arr[j]); - } else if (decide < 0.5) { - trie1.delete(arr[j]); - trie2.delete(arr[j]); - right.delete(arr[j]); - } else if (decide < 0.75) { - int ans1 = trie1.search(arr[j]); - int ans2 = trie2.search(arr[j]); - int ans3 = right.search(arr[j]); - if (ans1 != ans2 || ans2 != ans3) { - System.out.println("Oops!"); - } - } else { - int ans1 = trie1.prefixNumber(arr[j]); - int ans2 = trie2.prefixNumber(arr[j]); - int ans3 = right.prefixNumber(arr[j]); - if (ans1 != ans2 || ans2 != ans3) { - System.out.println("Oops!"); - } - } - } + chs := []byte(pre) + node := root + index := 0 + for i := 0; i < len(chs); i++ { + index = int(chs[i] - 'a') + // pre走不到最后,就没有以pre作为前缀的字符串存在 + if node.Childes[index] == nil { + return 0 } - System.out.println("finish!"); - + node = node.Childes[index] } - + // 顺利走到最后,返回的pass就是有多少个字符串以当前pre为前缀的 + return node.Pass } - ``` +> Trie的孩子Childes,可以用一个map实现。可以容纳针对各种字符串的情况,实现自由扩展,make(map[int]*Node),表示字符的ascii码对应的节点映射。此处略 + + ## 1.2 不基于比较的排序-桶排序 > 例如:一个代表员工年龄的数组,排序。数据范围有限,对每个年龄做词频统计。arr[0~200] = 0,M=200 @@ -364,15 +149,13 @@ public class Code02_TrieTree { ### 1.2.1 计数排序 -```text -桶排序思想下的排序:计数排序 & 基数排序 +桶排序思想下的排序:计数排序 & 基数排序: 1、 桶排序思想下的排序都是不基于比较的排序 2、 时间复杂度为O(N),二维空间复杂复杂度为O(M) 3、 应用范围有限,需要样本的数据状况满足桶的划分 -``` > 缺点:与样本数据状况强相关。 @@ -380,148 +163,50 @@ public class Code02_TrieTree { > 应用条件:十进制数据,非负 -```text -[100,17,29,13,5,27] 进行排序 => - -1、找最高位的那个数的长度,这里100的长度为3,其他数前补0,得出 - -[100,017,029,013,005,027] - -2、 准备10个桶,对应的数字0~9号桶,每个桶是一个队列。根据样本按个位数字对应进桶,相同个位数字进入队列,再从0号桶以此倒出,队列先进先出。个位进桶再依次倒出,得出: - -[100,013,005,017,027,029] +如果对:[100,17,29,13,5,27] 进行排序: -3、 再把按照个位进桶倒出的样本,再按十位进桶,再按相同规则倒出得: +1、找最高位的那个数的长度,这里100的长度为3,其他数前补0,得出 [100,017,029,013,005,027] -[100,005,013,017,027,029] +2、 准备10个桶,对应的数字0~9号桶,每个桶是一个队列。根据样本按个位数字对应进桶,相同个位数字进入队列,再从0号桶以此倒出,队列先进先出。个位进桶再依次倒出,得出 [100,013,005,017,027,029] -4、再把得到的样本按百位进桶,倒出得: +3、 再把按照个位进桶倒出的样本,再按十位进桶,再按相同规则倒出得 [100,005,013,017,027,029] -[005,013,017,027,029,100] +4、再把得到的样本按百位进桶,倒出得 [005,013,017,027,029,100] 此时达到有序! -``` - > 思想:先按各位数字排序,各位数字排好序,再用十位数字的顺序去调整,再按百位次序调整。优先级依次递增,百位优先级最高,百位优先级一样默认按照上一层十位的顺序... -==结论:基于比较的排序,时间复杂度的极限就是O(NlogN),而不基于比较的排序,时间复杂度可以达到O(N)。在面试或刷题,估算排序的时间复杂度的时候,必须用基于比较的排序来估算== +**结论:基于比较的排序,时间复杂度的极限就是O(NlogN),而不基于比较的排序,时间复杂度可以达到O(N)。在面试或刷题,估算排序的时间复杂度的时候,必须用基于比较的排序来估算** -```Java -/** -* 计数排序 -**/ -package class05; +```Go +package main -import java.util.Arrays; +import "math" -public class Code03_CountSort { - - // 计数排序 - // only for 0~200 value - public static void countSort(int[] arr) { - if (arr == null || arr.length < 2) { - return; - } - int max = Integer.MIN_VALUE; - for (int i = 0; i < arr.length; i++) { - max = Math.max(max, arr[i]); - } - int[] bucket = new int[max + 1]; - for (int i = 0; i < arr.length; i++) { - bucket[arr[i]]++; - } - int i = 0; - for (int j = 0; j < bucket.length; j++) { - while (bucket[j]-- > 0) { - arr[i++] = j; - } - } +// BucketSort 计数排序 +func BucketSort(arr []int) { + if len(arr) < 2 { + return } - // for test - public static void comparator(int[] arr) { - Arrays.sort(arr); + max := math.MinInt + for i := 0; i < len(arr); i++ { + max = int(math.Max(float64(max), float64(arr[i]))) } - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()); - } - return arr; + bucket := make([]int, max+1) + for i := 0; i < len(arr); i++ { + bucket[arr[i]]++ } - - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; + k := 0 + for i := 0; i < len(bucket); i++ { + bucket[i]-- + for bucket[i] > 0 { + arr[k] = i + k++ } - return res; } - - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; - } - - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - - // for test - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 100; - int maxValue = 150; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - countSort(arr1); - comparator(arr2); - if (!isEqual(arr1, arr2)) { - succeed = false; - printArray(arr1); - printArray(arr2); - break; - } - } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - - int[] arr = generateRandomArray(maxSize, maxValue); - printArray(arr); - countSort(arr); - printArray(arr); - - } - } ``` @@ -535,171 +220,72 @@ public class Code03_CountSort { > 实质上,基数排序的时间复杂度是O(Nlog10max(N)),log10N表示十进制的数的位数,但是我们认为基数排序的应用样本范围不大。如果要排任意位数的值,严格上就是O(Nlog10max(N)) -```Java -/** -* 基数排序 -**/ -package class05; +```Go +package main -import java.util.Arrays; +import "fmt" -public class Code04_RadixSort { +func RadixSort(nums []int) []int { + numberBit := howManyBit(maximum(nums)) + // 循环的次数 + // 定义一个rec 二维切片 rec[i][x] 用来接受尾数是 i的数字 + for i := 0; i < numberBit; i++ { + rec := make([][]int, 10) - // 非负数,十进制,如果负数需要深度改写这个方法 - // only for no-negative value - public static void radixSort(int[] arr) { - if (arr == null || arr.length < 2) { - return; + for _, num := range nums { + rec[(num/pow10(i))%10] = append(rec[(num/pow10(i))%10], num) } - radixSort(arr, 0, arr.length - 1, maxbits(arr)); - } - - // 计算数组样本中最大值的位数 - public static int maxbits(int[] arr) { - int max = Integer.MIN_VALUE; - for (int i = 0; i < arr.length; i++) { - max = Math.max(max, arr[i]); - } - int res = 0; - while (max != 0) { - res++; - max /= 10; - } - return res; - } - - // arr[l..r]排序 , digit:最大值的位数 - // l..r [3, 56, 17, 100] 3 - public static void radixSort(int[] arr, int L, int R, int digit) { - // 由于十进制的数,我们依10位基底 - final int radix = 10; - int i = 0, j = 0; - // 有多少个数准备多少个辅助空间 - int[] help = new int[R - L + 1]; - for (int d = 1; d <= digit; d++) { // 有多少位就进出几次 - // 10个空间 - // count[0] 当前位(d位)是0的数字有多少个 - // count[1] 当前位(d位)是(0和1)的数字有多少个 - // count[2] 当前位(d位)是(0、1和2)的数字有多少个 - // count[i] 当前位(d位)是(0~i)的数字有多少个 - int[] count = new int[radix]; // count[0..9] - for (i = L; i <= R; i++) { - // 103的话 d是1表示个位 取出j=3 - // 209 1 9 - j = getDigit(arr[i], d); - count[j]++; - } - // conut往conut'的转化 - for (i = 1; i < radix; i++) { - count[i] = count[i] + count[i - 1]; - } - // i从最后位置往前看 - for (i = R; i >= L; i--) { - j = getDigit(arr[i], d); - help[count[j] - 1] = arr[i]; - // 词频-- - count[j]--; - } - // 处理完个位十位...之后都要往原数组copy - for (i = L, j = 0; i <= R; i++, j++) { - arr[i] = help[j]; - } - } - - - - - } - - public static int getDigit(int x, int d) { - return ((x / ((int) Math.pow(10, d - 1))) % 10); - } - - // for test - public static void comparator(int[] arr) { - Arrays.sort(arr); - } - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) ((maxValue + 1) * Math.random()); - } - return arr; - } - - // for test - public static int[] copyArray(int[] arr) { - if (arr == null) { - return null; - } - int[] res = new int[arr.length]; - for (int i = 0; i < arr.length; i++) { - res[i] = arr[i]; + // flatten the rec slice to the one dimension slice + numsCopy := make([]int, 0) + for j := 0; j < 10; j++ { + numsCopy = append(numsCopy, rec[j]...) } - return res; + // refresh nums,使得他变为 经过一次基数排序之后的数组 + nums = numsCopy } + return nums +} - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; +func pow10(num int) int { + res := 1 + base := 10 + for num != 0 { + if num&1 ==1 { + num -= 1 + res *= base } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; + num >>= 1 + base *= base } + return res +} - // for test - public static void printArray(int[] arr) { - if (arr == null) { - return; +func maximum(list []int) int { + max := 0 + for _, i2 := range list { + if i2 > max { + max = i2 } - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); } + return max +} - // for test - public static void main(String[] args) { - int testTime = 500000; - int maxSize = 100; - int maxValue = 100000; - boolean succeed = true; - for (int i = 0; i < testTime; i++) { - int[] arr1 = generateRandomArray(maxSize, maxValue); - int[] arr2 = copyArray(arr1); - radixSort(arr1); - comparator(arr2); - if (!isEqual(arr1, arr2)) { - succeed = false; - printArray(arr1); - printArray(arr2); - break; - } - } - System.out.println(succeed ? "Nice!" : "Fucking fucked!"); - - int[] arr = generateRandomArray(maxSize, maxValue); - printArray(arr); - radixSort(arr); - printArray(arr); - +func howManyBit(number int) int { + count := 0 + for number != 0 { + number = number/10 + count += 1 } - + return count } +func main() { + var theArray = []int{10, 1, 18, 30, 23, 12, 7, 5, 18, 233, 144} + fmt.Print("排序前") + fmt.Println(theArray) + fmt.Print("排序后") + fmt.Println(RadixSort(theArray)) +} ``` ## 1.3 排序算法的稳定性 diff --git "a/06-\351\223\276\350\241\250\347\233\270\345\205\263\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" "b/06-\351\223\276\350\241\250\347\233\270\345\205\263\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" deleted file mode 100644 index b910c7c..0000000 --- "a/06-\351\223\276\350\241\250\347\233\270\345\205\263\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" +++ /dev/null @@ -1,1013 +0,0 @@ -[TOC] -# 1 链表问题 - -> 面试时链表解题的方法论 - -> 对于笔试,不用太在乎空间复杂度,一切为了时间复杂度 - -> 对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法 - -## 1.1 链表面试常用数据结构和技巧 - -1、 使用容器(哈希表,数组等) - -2、 快慢指针 - -### 1.1.1 快慢指针问题 - -1、 输入链表头结点,奇数长度返回中点,偶数长度返回上中点 - -> 1 3 5 2 7 返回 5;1 3 2 7 返回 3 - -2、 输入链表头结点,奇数长度返回中点,偶数长度返回中下点 - -> 1 3 5 2 7 返回 5;1 3 2 7 返回 2 - -3、 输入链表头结点,奇数长度返回中点前一个,偶数长度返回上中点前一个 - -> 1 3 5 2 7 返回 3;1 3 2 7 返回 1 - -4、 输入链表头结点,奇数长度返回中点前一个,偶数长度返回下中点前一个 - -> 1 3 5 2 7 返回 3;1 3 2 7 返回 3 - -```Java -package class06; - -import java.util.ArrayList; - -public class Code01_LinkedListMid { - - public static class Node { - public int value; - public Node next; - - public Node(int v) { - value = v; - } - } - - // head 头 - // 1、奇数长度返回中点,偶数长度返回上中点 - public static Node midOrUpMidNode(Node head) { - // 没有点,有一个点,有两个点的时候都是返回头结点 - if (head == null || head.next == null || head.next.next == null) { - return head; - } - // 链表有3个点或以上 - // 快慢指针,快指针一次走两步,慢指针一次走一步 - // 快指针走完,慢指针在中点位置 - Node slow = head.next; - Node fast = head.next.next; - while (fast.next != null && fast.next.next != null) { - slow = slow.next; - fast = fast.next.next; - } - return slow; - } - - // 2、奇数长度返回中点,偶数长度返回中下点 - public static Node midOrDownMidNode(Node head) { - if (head == null || head.next == null) { - return head; - } - Node slow = head.next; - Node fast = head.next; - while (fast.next != null && fast.next.next != null) { - slow = slow.next; - fast = fast.next.next; - } - return slow; - } - - // 3、奇数长度返回中点前一个,偶数长度返回上中点前一个 - public static Node midOrUpMidPreNode(Node head) { - if (head == null || head.next == null || head.next.next == null) { - return null; - } - Node slow = head; - Node fast = head.next.next; - while (fast.next != null && fast.next.next != null) { - slow = slow.next; - fast = fast.next.next; - } - return slow; - } - - // 4、奇数长度返回中点前一个,偶数长度返回下中点前一个 - public static Node midOrDownMidPreNode(Node head) { - if (head == null || head.next == null) { - return null; - } - if (head.next.next == null) { - return head; - } - Node slow = head; - Node fast = head.next; - while (fast.next != null && fast.next.next != null) { - slow = slow.next; - fast = fast.next.next; - } - return slow; - } - - // 笔试可以用这种复杂点的方法,空间复杂度比上面快慢指针要高 - public static Node right1(Node head) { - if (head == null) { - return null; - } - Node cur = head; - ArrayList arr = new ArrayList<>(); - while (cur != null) { - arr.add(cur); - cur = cur.next; - } - return arr.get((arr.size() - 1) / 2); - } - - public static Node right2(Node head) { - if (head == null) { - return null; - } - Node cur = head; - ArrayList arr = new ArrayList<>(); - while (cur != null) { - arr.add(cur); - cur = cur.next; - } - return arr.get(arr.size() / 2); - } - - public static Node right3(Node head) { - if (head == null || head.next == null || head.next.next == null) { - return null; - } - Node cur = head; - ArrayList arr = new ArrayList<>(); - while (cur != null) { - arr.add(cur); - cur = cur.next; - } - return arr.get((arr.size() - 3) / 2); - } - - public static Node right4(Node head) { - if (head == null || head.next == null) { - return null; - } - Node cur = head; - ArrayList arr = new ArrayList<>(); - while (cur != null) { - arr.add(cur); - cur = cur.next; - } - return arr.get((arr.size() - 2) / 2); - } - - public static void main(String[] args) { - Node test = null; - test = new Node(0); - test.next = new Node(1); - test.next.next = new Node(2); - test.next.next.next = new Node(3); - test.next.next.next.next = new Node(4); - test.next.next.next.next.next = new Node(5); - test.next.next.next.next.next.next = new Node(6); - test.next.next.next.next.next.next.next = new Node(7); - test.next.next.next.next.next.next.next.next = new Node(8); - - Node ans1 = null; - Node ans2 = null; - - ans1 = midOrUpMidNode(test); - ans2 = right1(test); - System.out.println(ans1 != null ? ans1.value : "无"); - System.out.println(ans2 != null ? ans2.value : "无"); - - ans1 = midOrDownMidNode(test); - ans2 = right2(test); - System.out.println(ans1 != null ? ans1.value : "无"); - System.out.println(ans2 != null ? ans2.value : "无"); - - ans1 = midOrUpMidPreNode(test); - ans2 = right3(test); - System.out.println(ans1 != null ? ans1.value : "无"); - System.out.println(ans2 != null ? ans2.value : "无"); - - ans1 = midOrDownMidPreNode(test); - ans2 = right4(test); - System.out.println(ans1 != null ? ans1.value : "无"); - System.out.println(ans2 != null ? ans2.value : "无"); - - } - -} -``` - -### 1.1.2 面试题一:判断回文结构 - -> 给定一个单链表的头结点head,请判断该链表是否为回文结构。回文就是正着输出和反着输出结果一样 - -1. 栈的方法特别简单(笔试用) - -> 笔试思路,以此把该链表放入栈中。再遍历该链表和栈中弹出的数比对,只要有不一样,就不是回文 - -2. 改原链表的方法需要注意边界问题(面试用) - -> 快慢指针解法:用快慢指针定位到中点的位置,奇数就是定位到唯一的中点,偶数定位到上中点。然后把中点右半部分加入栈中去,那么栈中存的是右半部分的逆序。接着从头遍历链表,栈中有多少个元素,我们就比较多少步,如果有对不上就不是回文 - -> 快慢指针最优解,不使用容器结构(stack),O(1):同样的找到中点位置,把右半部分指针回指到中点。接着指针1从L位置,指针2从R位置,往中间遍历。,每步比对,如果有不一样,则不是回文。返回答案之前,把中点右边的指针调整回来 - -```Java -package class06; - -import java.util.Stack; - -public class Code02_IsPalindromeList { - - public static class Node { - public int value; - public Node next; - - public Node(int data) { - this.value = data; - } - } - - // need n extra space - public static boolean isPalindrome1(Node head) { - // 依次进栈 - Stack stack = new Stack(); - Node cur = head; - while (cur != null) { - stack.push(cur); - cur = cur.next; - } - // 每个元素和栈中比较 - while (head != null) { - if (head.value != stack.pop().value) { - return false; - } - head = head.next; - } - return true; - } - - // need n/2 extra space - // 中点右侧进栈 - public static boolean isPalindrome2(Node head) { - if (head == null || head.next == null) { - return true; - } - Node right = head.next; - Node cur = head; - while (cur.next != null && cur.next.next != null) { - right = right.next; - cur = cur.next.next; - } - Stack stack = new Stack(); - while (right != null) { - stack.push(right); - right = right.next; - } - while (!stack.isEmpty()) { - if (head.value != stack.pop().value) { - return false; - } - head = head.next; - } - return true; - } - - // need O(1) extra space - // 不使用容器(stack)的方法 - public static boolean isPalindrome3(Node head) { - if (head == null || head.next == null) { - return true; - } - // 慢指针 - Node n1 = head; - // 快指针 - Node n2 = head; - while (n2.next != null && n2.next.next != null) { // find mid node - n1 = n1.next; // n1 -> mid - n2 = n2.next.next; // n2 -> end - } - // n1 中点 - - - n2 = n1.next; // n2 -> right part first node - n1.next = null; // mid.next -> null - Node n3 = null; - // 右半部逆序指向中点 - while (n2 != null) { // right part convert - n3 = n2.next; // n3 -> save next node - n2.next = n1; // next of right node convert - n1 = n2; // n1 move - n2 = n3; // n2 move - } - // 引入n3记录最后的位置,之后把右半部再逆序回原来的次序 - n3 = n1; // n3 -> save last node - n2 = head;// n2 -> left first node - boolean res = true; - while (n1 != null && n2 != null) { // check palindrome - if (n1.value != n2.value) { - res = false; - break; - } - n1 = n1.next; // left to mid - n2 = n2.next; // right to mid - } - n1 = n3.next; - n3.next = null; - // 把右半部分再逆序回来 - while (n1 != null) { // recover list - n2 = n1.next; - n1.next = n3; - n3 = n1; - n1 = n2; - } - return res; - } - - public static void printLinkedList(Node node) { - System.out.print("Linked List: "); - while (node != null) { - System.out.print(node.value + " "); - node = node.next; - } - System.out.println(); - } - - public static void main(String[] args) { - - Node head = null; - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - head.next = new Node(2); - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - head.next = new Node(1); - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - head.next = new Node(2); - head.next.next = new Node(3); - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - head.next = new Node(2); - head.next.next = new Node(1); - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - head.next = new Node(2); - head.next.next = new Node(3); - head.next.next.next = new Node(1); - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - head.next = new Node(2); - head.next.next = new Node(2); - head.next.next.next = new Node(1); - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - head.next = new Node(2); - head.next.next = new Node(3); - head.next.next.next = new Node(2); - head.next.next.next.next = new Node(1); - printLinkedList(head); - System.out.print(isPalindrome1(head) + " | "); - System.out.print(isPalindrome2(head) + " | "); - System.out.println(isPalindrome3(head) + " | "); - printLinkedList(head); - System.out.println("========================="); - - } - -} - -``` - -### 1.1.3 面试题二:按值划分单链表 - -> 将单链表按某值划分成左边小,中间相等,右边大的形式 - -1. 把链表放入数组里,在数组上做partion(笔试用) - -> [3, 2, 4, 7, 0, 2, 1]选择2划分,基于2对数组作partion - -2. 分成小、中、大三部分。再把各个部分之间串起来(面试用) - -> 借助6个引用变量,不需要容器O(N),且能保证稳定性。小于区域的头引用,小于区域的尾引用,等于区域的头引用,等于区域的尾引用,大于区域的头引用,大于区域的尾引用。依次对比给定的值加入到这三个区域,之后串联起来 - -```Java -package class06; - -public class Code03_SmallerEqualBigger { - - public static class Node { - public int value; - public Node next; - - public Node(int data) { - this.value = data; - } - } - - // 方法1 - public static Node listPartition1(Node head, int pivot) { - if (head == null) { - return head; - } - Node cur = head; - int i = 0; - while (cur != null) { - i++; - cur = cur.next; - } - Node[] nodeArr = new Node[i]; - i = 0; - cur = head; - for (i = 0; i != nodeArr.length; i++) { - nodeArr[i] = cur; - cur = cur.next; - } - arrPartition(nodeArr, pivot); - for (i = 1; i != nodeArr.length; i++) { - nodeArr[i - 1].next = nodeArr[i]; - } - nodeArr[i - 1].next = null; - // 返回头结点 - return nodeArr[0]; - } - - public static void arrPartition(Node[] nodeArr, int pivot) { - int small = -1; - int big = nodeArr.length; - int index = 0; - while (index != big) { - if (nodeArr[index].value < pivot) { - swap(nodeArr, ++small, index++); - } else if (nodeArr[index].value == pivot) { - index++; - } else { - swap(nodeArr, --big, index); - } - } - } - - public static void swap(Node[] nodeArr, int a, int b) { - Node tmp = nodeArr[a]; - nodeArr[a] = nodeArr[b]; - nodeArr[b] = tmp; - } - - // 方法2 - public static Node listPartition2(Node head, int pivot) { - Node sH = null; // small head - Node sT = null; // small tail - Node eH = null; // equal head - Node eT = null; // equal tail - Node mH = null; // big head - Node mT = null; // big tail - Node next = null; // save next node - // every node distributed to three lists - while (head != null) { - next = head.next; - head.next = null; - if (head.value < pivot) { - // 小于节点为空,当前节点即做头又做尾 - if (sH == null) { - sH = head; - sT = head; - // 老的尾节点指向当前节点,老的尾变成当前节点 - } else { - sT.next = head; - sT = head; - } - } else if (head.value == pivot) { - if (eH == null) { - eH = head; - eT = head; - } else { - eT.next = head; - eT = head; - } - } else { - if (mH == null) { - mH = head; - mT = head; - } else { - mT.next = head; - mT = head; - } - } - head = next; - } - // 小于区域的尾巴,连等于区域的头,等于区域的尾巴连大于区域的头 - if (sT != null) { // 如果有小于区域 - sT.next = eH; - eT = eT == null ? sT : eT; // 下一步,谁去连大于区域的头,谁就变成eT - } - // 上面的if,不管跑了没有,et - // all reconnect - if (eT != null) { // 如果小于区域和等于区域,不是都没有 - eT.next = mH; - } - return sH != null ? sH : (eH != null ? eH : mH); - } - - public static void printLinkedList(Node node) { - System.out.print("Linked List: "); - while (node != null) { - System.out.print(node.value + " "); - node = node.next; - } - System.out.println(); - } - - public static void main(String[] args) { - Node head1 = new Node(7); - head1.next = new Node(9); - head1.next.next = new Node(1); - head1.next.next.next = new Node(8); - head1.next.next.next.next = new Node(5); - head1.next.next.next.next.next = new Node(2); - head1.next.next.next.next.next.next = new Node(5); - printLinkedList(head1); - // head1 = listPartition1(head1, 4); - head1 = listPartition2(head1, 5); - printLinkedList(head1); - - } - -} -``` - -### 1.1.4 面试题三 - -> 一种特殊的单链表结构如下: - -```Java -public static class Node { - public int value; - public Node next; - public Node rand; - - public Node(int data) { - this.value = data; - } - } -``` - -> rand指针式单链表节点结构中新增加的指针,rand可能指向链表中的任意一个节点,也可能为null。给定一个由Node节点类型组成的无环单链表节点head。请实现一个函数完成这个链表的复制,并返回复制的新链表的头结点。 - -> 要求时间复杂度为O(N),额外空间复杂度为O(1) - -1. 哈希表方法(笔试推荐) - -> 第一步遍历,把所有节点加入到Map表示老节点到克隆出来的节点映射 - -> 第二步遍历,查map找到克隆节点,最后返回头结点 - -2. 不用哈希表的方法,人为构造对应关系(面试推荐) - -> 第一步:每个节点遍历的时候克隆出来一个新的节点加入到当前节点和其next节点的中间 - -> 第二步:此时经过第一步所有节点和其克隆节点都是串在一起的,依次拿出当前节点和其克隆节点,当前节点的rand指针指向的节点的克隆节点给当前节点克隆的节点的rand节点指向的节点。 - -> 第三步:此时老节点的rand指针没变化,克隆节点的rand指针也都指向了对应的克隆节点。此时在大的链表上分离出来原链表和克隆链表 - -```Java -package class06; - -import java.util.HashMap; - -public class Code04_CopyListWithRandom { - - public static class Node { - public int value; - public Node next; - public Node rand; - - public Node(int data) { - this.value = data; - } - } - - // 方法1 - public static Node copyListWithRand1(Node head) { - HashMap map = new HashMap(); - Node cur = head; - while (cur != null) { - // 当前节点,克隆出来一个相同值的新节点加入字典 - map.put(cur, new Node(cur.value)); - cur = cur.next; - } - // 当前节点从头开始 - cur = head; - while (cur != null) { - // cur 老 - // map.get(cur) 新 - map.get(cur).next = map.get(cur.next); - map.get(cur).rand = map.get(cur.rand); - cur = cur.next; - } - // 返回head对应的克隆节点 - return map.get(head); - } - - // 方法二 - public static Node copyListWithRand2(Node head) { - if (head == null) { - return null; - } - Node cur = head; - Node next = null; - // 克隆出来的node放在原本node和next指向的node中间 - // 1 -> 2 - // 1 -> 1' -> 2 - while (cur != null) { - // cur 老 next 老的下一个 - next = cur.next; - cur.next = new Node(cur.value); - cur.next.next = next; - cur = next; - } - cur = head; - Node curCopy = null; - // set copy node rand - // 1 -> 1' -> 2 -> 2' - // 设置新的克隆节点间的rand节点 - while (cur != null) { - // cur 老 - // cur.next => 新的 copy出来的节点 - next = cur.next.next; - curCopy = cur.next; - curCopy.rand = cur.rand != null ? cur.rand.next : null; - cur = next; - } - // 老的头结点:head 新克隆出来的头结点: head.next - Node res = head.next; - cur = head; - // split,分离原本节点组成的链表和克隆节点组成的链表 - while (cur != null) { - next = cur.next.next; - curCopy = cur.next; - cur.next = next; - curCopy.next = next != null ? next.next : null; - cur = next; - } - return res; - } - - public static void printRandLinkedList(Node head) { - Node cur = head; - System.out.print("order: "); - while (cur != null) { - System.out.print(cur.value + " "); - cur = cur.next; - } - System.out.println(); - cur = head; - System.out.print("rand: "); - while (cur != null) { - System.out.print(cur.rand == null ? "- " : cur.rand.value + " "); - cur = cur.next; - } - System.out.println(); - } - - public static void main(String[] args) { - Node head = null; - Node res1 = null; - Node res2 = null; - printRandLinkedList(head); - res1 = copyListWithRand1(head); - printRandLinkedList(res1); - res2 = copyListWithRand2(head); - printRandLinkedList(res2); - printRandLinkedList(head); - System.out.println("========================="); - - head = new Node(1); - head.next = new Node(2); - head.next.next = new Node(3); - head.next.next.next = new Node(4); - head.next.next.next.next = new Node(5); - head.next.next.next.next.next = new Node(6); - - head.rand = head.next.next.next.next.next; // 1 -> 6 - head.next.rand = head.next.next.next.next.next; // 2 -> 6 - head.next.next.rand = head.next.next.next.next; // 3 -> 5 - head.next.next.next.rand = head.next.next; // 4 -> 3 - head.next.next.next.next.rand = null; // 5 -> null - head.next.next.next.next.next.rand = head.next.next.next; // 6 -> 4 - - printRandLinkedList(head); - res1 = copyListWithRand1(head); - printRandLinkedList(res1); - res2 = copyListWithRand2(head); - printRandLinkedList(res2); - printRandLinkedList(head); - System.out.println("========================="); - - } - -} -``` - -### 1.1.5 面试题四 - -> 该问题和约舍夫环问题是链表问题的比较难的问题 - -> 题目描述:给定两个可能有环也可能无环的单链表,头结点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null - -> 要求:如果两个链表长度之和为N,时间复杂度请达到O(N),额为空间复杂度请达到O(1) - -> 思路:由于是单链表,则一旦成环就结束,出不来,因为每个节点只有一个Next指针 - -1. 方法一:用set把每个节点的内存地址放到set里面,如果存在相同的内存在set中存在,则就是第一个成环的节点 - - -2. 用快慢指针对链表遍历,那么快慢指针一定会相遇,能相遇就说明存在环。然后让慢指针停在相遇的位置,快指针回到头结点。快指针和慢指针再出发且快指针也变成一次走一步和满指针相同,再次相遇的节点就是成环节点 - -```Java -package class06; - -public class Code05_FindFirstIntersectNode { - - public static class Node { - public int value; - public Node next; - - public Node(int data) { - this.value = data; - } - } - - public static Node getIntersectNode(Node head1, Node head2) { - if (head1 == null || head2 == null) { - return null; - } - // head1的第一个入环节点 - Node loop1 = getLoopNode(head1); - // head2的第一个入环节点 - Node loop2 = getLoopNode(head2); - // 两个无环链表是否相交的情况 - // 由于每个节点只有一个next指针,则如果两个无环相交则相交之后就只剩下公共部分 - // 方法1把第一条链表放到set中,第二个链表依次查在不在该set中,第一个找到的就是 - // 方法2 - // 把链表1走到尾结点end1,记录长度l1 - // 把链表1走到尾结点end2,记录长度l2 - // 如果end1和end2的内存地址不同一定不相交 - // 如果end1==end2,则(1)长的链表从头结点先走保证和短链表相同长度的位置,再以此往下走,第一次相同节点 - // (2)则依次从尾结点出发,找第一次出现内存地址不相同的那个节点,该节点的next节点就是第一次相交的节点 - if (loop1 == null && loop2 == null) { - return noLoop(head1, head2); - } - - // 一个为空,另外一个不为空不可能相交。两个都不为空的情况下共用一个环 - if (loop1 != null && loop2 != null) { - return bothLoop(head1, loop1, head2, loop2); - } - return null; - } - - // 找到链表第一个入环节点,如果无环,返回null - public static Node getLoopNode(Node head) { - if (head == null || head.next == null || head.next.next == null) { - return null; - } - // n1 慢 n2 快 - Node n1 = head.next; // n1 -> slow - Node n2 = head.next.next; // n2 -> fast - while (n1 != n2) { - if (n2.next == null || n2.next.next == null) { - return null; - } - n2 = n2.next.next; - n1 = n1.next; - } - // 能相遇则跳出while,快指针回到开头,满指针停在原地 - n2 = head; // n2 -> walk again from head - while (n1 != n2) { - // 此时快慢指针每次移动相同步数 - n1 = n1.next; - n2 = n2.next; - } - return n1; - } - - // 如果两个链表都无环,返回第一个相交节点,如果不想交,返回null - public static 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; - } - // n : 链表1长度减去链表2长度的值 - // 谁长,谁的头变成cur1 - cur1 = n > 0 ? head1 : head2; - // 谁短,谁的头变成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; - } - - // 两个有环链表,返回第一个相交节点,如果不想交返回null - // head1的入环节点是loop1,head2的入环节点是loop2 - public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) { - Node cur1 = null; - Node cur2 = null; - // 类似第一种都无环的情况 - if (loop1 == loop2) { - cur1 = head1; - cur2 = head2; - int n = 0; - while (cur1 != loop1) { - n++; - cur1 = cur1.next; - } - while (cur2 != loop2) { - n--; - cur2 = cur2.next; - } - cur1 = n > 0 ? head1 : head2; - 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; - } else { - //否则,找第一个成环节点转回自身的过程中遇到loop2,则相交,否则不相交 - cur1 = loop1.next; - while (cur1 != loop1) { - if (cur1 == loop2) { - return loop1; - } - cur1 = cur1.next; - } - return null; - } - } - - public static void main(String[] args) { - // 1->2->3->4->5->6->7->null - Node head1 = new Node(1); - head1.next = new Node(2); - head1.next.next = new Node(3); - head1.next.next.next = new Node(4); - head1.next.next.next.next = new Node(5); - head1.next.next.next.next.next = new Node(6); - head1.next.next.next.next.next.next = new Node(7); - - // 0->9->8->6->7->null - Node head2 = new Node(0); - head2.next = new Node(9); - head2.next.next = new Node(8); - head2.next.next.next = head1.next.next.next.next.next; // 8->6 - System.out.println(getIntersectNode(head1, head2).value); - - // 1->2->3->4->5->6->7->4... - head1 = new Node(1); - head1.next = new Node(2); - head1.next.next = new Node(3); - head1.next.next.next = new Node(4); - head1.next.next.next.next = new Node(5); - head1.next.next.next.next.next = new Node(6); - head1.next.next.next.next.next.next = new Node(7); - head1.next.next.next.next.next.next = head1.next.next.next; // 7->4 - - // 0->9->8->2... - head2 = new Node(0); - head2.next = new Node(9); - head2.next.next = new Node(8); - head2.next.next.next = head1.next; // 8->2 - System.out.println(getIntersectNode(head1, head2).value); - - // 0->9->8->6->4->5->6.. - head2 = new Node(0); - head2.next = new Node(9); - head2.next.next = new Node(8); - head2.next.next.next = head1.next.next.next.next.next; // 8->6 - System.out.println(getIntersectNode(head1, head2).value); - - } - -} -``` - -### 1.1.6 面试题五 - -> 题目描述:能不能不给单链表的头结点,只给想要删除的节点,就能做到在链表上把这个点删掉? - -1. 抖机灵的做法,1->2->3->4->5->null,给定3。那么根据内存地址找到3这个节点,把3下个节点赋值给自身变成4,再把自身的下一个指针指向下下个值5即可。1->2->4->5->null。缺点没把原始节点删除,只是改变了值,内存地址没被删掉而是删掉了需要删除节点的下一个内存地址。该方法无法删除链表的最后一个节点 - - -> 实质上不给头结点,无法删除给定的节点。没有头结点,没法准确的连指针 - -```Java - -package class06; - -public class Test { - - public static class Node{ - public int value; - public Node next; - public Node(int v) { - value = v; - } - } - - public static void main(String[] args) { - Node a = new Node(1); - Node b = new Node(2); - Node c = new Node(3); - - a.next = b; - b.next = c; - // 实质上这里置为空没用,只是把Java栈中的变量不指向堆中的3节点 - //堆中的结构没改变,3节点并没有被删除 - c = null; - - } - -} - -``` - diff --git "a/06-\351\223\276\350\241\250\347\233\270\345\205\263\351\253\230\351\242\221\351\242\230\346\200\273\347\273\223.md" "b/06-\351\223\276\350\241\250\347\233\270\345\205\263\351\253\230\351\242\221\351\242\230\346\200\273\347\273\223.md" new file mode 100644 index 0000000..2203c20 --- /dev/null +++ "b/06-\351\223\276\350\241\250\347\233\270\345\205\263\351\253\230\351\242\221\351\242\230\346\200\273\347\273\223.md" @@ -0,0 +1,588 @@ +[TOC] +# 1 链表问题 + +> 面试时链表解题的方法论 + +> 对于笔试,不用太在乎空间复杂度,一切为了时间复杂度 + +> 对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法 + +## 1.1 链表面试常用数据结构和技巧 + +1、 使用容器(哈希表,数组等) + +2、 快慢指针 + +### 1.1.1 快慢指针问题 + +1、 输入链表头结点,奇数长度返回中点,偶数长度返回上中点 + +> 1 3 5 2 7 返回 5;1 3 2 7 返回 3 + +2、 输入链表头结点,奇数长度返回中点,偶数长度返回中下点 + +> 1 3 5 2 7 返回 5;1 3 2 7 返回 2 + +3、 输入链表头结点,奇数长度返回中点前一个,偶数长度返回上中点前一个 + +> 1 3 5 2 7 返回 3;1 3 2 7 返回 1 + +4、 输入链表头结点,奇数长度返回中点前一个,偶数长度返回下中点前一个 + +> 1 3 5 2 7 返回 3;1 3 2 7 返回 3 + +```Go +package main + +import "fmt" + +type Node struct { + Val int + Next *Node +} + +// NewLinkedList 初始化一个链表 返回链表的头结点 +func NewLinkedList(val int) (head *Node) { + return &Node{ + Val: val, + Next: nil, + } +} + +// MidOrUpMidNode 给定一个链表的头节点 +// 1. 奇数长度返回中点, 偶数长度返回上中点 +func (head *Node) MidOrUpMidNode() *Node { + // 该链表没有点,有一个点,有两个点的时候都是返回头结点 + if head == nil || head.Next == nil || head.Next.Next == nil { + return head + } + + // 链表有3个点或以上 + // 快慢指针,快指针一次走两步,慢指针一次走一步 + // 快指针走完,慢指针在中点位置 + slow := head.Next + fast := head.Next.Next + for fast.Next != nil && fast.Next.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + return slow +} + +// MidOrDownMidNode 给定一个链表的头节点 +// 2、奇数长度返回中点,偶数长度返回中下点 +func (head *Node) MidOrDownMidNode() *Node { + // 该链表没有点,有一个点, 返回头结点 + if head == nil || head.Next == nil { + return head + } + slow := head.Next + fast := head.Next + for fast.Next != nil && fast.Next.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + return slow +} + +// MidOrUpMidPreNode 给定一个链表的头节点 +// 3、奇数长度返回中点前一个,偶数长度返回上中点前一个 +func (head *Node) MidOrUpMidPreNode() *Node { + // 该链表没有点,有一个点, 有两个点, 返回头结点 + if head == nil || head.Next == nil || head.Next.Next == nil { + return nil + } + slow := head + fast := head.Next.Next + for fast.Next != nil && fast.Next.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + return slow +} + +// MidOrDownMidPreNode 给定一个链表的头节点 +// 4、奇数长度返回中点前一个,偶数长度返回下中点前一个 +func (head *Node) MidOrDownMidPreNode() *Node { + if head == nil || head.Next == nil { + return nil + } + if head.Next.Next == nil { + return head + } + slow := head + fast := head.Next + for fast.Next != nil && fast.Next.Next != nil { + slow = slow.Next + fast = fast.Next.Next + } + return slow +} + +func main() { + // 0 1 + // 0 1 2 + // 0 1 2 3 + // 0 1 2 3 4 + // 0 1 2 3 4 5 + // 0 1 2 3 4 5 6 + // 0 1 2 3 4 5 6 7 + // 0 1 2 3 4 5 6 7 8 + hd := &Node{} + hd.Next = &Node{Val: 1} + hd.Next.Next = &Node{Val: 2} + hd.Next.Next.Next = &Node{Val: 3} + hd.Next.Next.Next.Next = &Node{Val: 4} + hd.Next.Next.Next.Next.Next = &Node{Val: 5} + hd.Next.Next.Next.Next.Next.Next = &Node{Val: 6} + hd.Next.Next.Next.Next.Next.Next.Next = &Node{Val: 7} + // hd.Next.Next.Next.Next.Next.Next.Next.Next = &Node{Val: 8} + + ans1 := hd.MidOrUpMidNode() + fmt.Println(fmt.Sprintf("1.奇数长度返回中点,偶数长度返回上中点: %d", ans1.Val)) + ans2 := hd.MidOrDownMidNode() + fmt.Println(fmt.Sprintf("2.奇数长度返回中点,偶数长度返回中下点: %d", ans2.Val)) + ans3 := hd.MidOrUpMidPreNode() + fmt.Println(fmt.Sprintf("3.奇数长度返回中点前一个,偶数长度返回上中点前一个: %d", ans3.Val)) + ans4 := hd.MidOrDownMidPreNode() + fmt.Println(fmt.Sprintf("4.奇数长度返回中点前一个,偶数长度返回下中点前一个: %d", ans4.Val)) +} +``` + +### 1.1.2 面试题一:判断回文结构 + +> 给定一个单链表的头结点head,请判断该链表是否为回文结构。回文就是正着输出和反着输出结果一样 + +1. 栈的方法特别简单(笔试用) + +> 笔试思路,以此把该链表放入栈中。再遍历该链表和栈中弹出的数比对,只要有不一样,就不是回文 + +2. 改原链表的方法需要注意边界问题(面试用) + +> 快慢指针解法:用快慢指针定位到中点的位置,奇数就是定位到唯一的中点,偶数定位到上中点。然后把中点右半部分加入栈中去,那么栈中存的是右半部分的逆序。接着从头遍历链表,栈中有多少个元素,我们就比较多少步,如果有对不上就不是回文 + +> 快慢指针最优解,不使用容器结构(stack),O(1):同样的找到中点位置,把右半部分指针回指到中点。接着指针1从L位置,指针2从R位置,往中间遍历。,每步比对,如果有不一样,则不是回文。返回答案之前,把中点右边的指针调整回来 + +```Go +package main + +// 判断一个链表是否是回文链表 +// 解法1:遍历链表,把链表放入一个数组中,倒序遍历数组切片,和正序遍历数组切片依次对比,都相等即为回文,实现略 +// 解法2:遍历链表,按照快慢指针使慢指针定位到链表的中点位置,依次把慢指针指向的值写入数组切片中,此时数组中保存着链表前一半的元素。 +// 且slow的位置是链表的中间位置。按倒序遍历数组切片,与按slow位置顺序遍历链表的元素依次对比,如果都相等,则为回文。实现略 +// 解法2比解法1省了一半的空间,解法一空间复杂度O(n),解法2空间复杂度为O(n/2) +// 解法3:不使用额外空间,空间复杂度为O(1) + +type Node struct { + Val int + Next *Node +} + +// IsPalindrome 给定链表头节点,判断该链表是不是回文链表。空间复杂度为O(1) +func (head *Node) IsPalindrome() bool { + // 链表为空,或者链表只有一个节点,是回文结构 + if head == nil || head.Next == nil { + return true + } + // 慢指针 + slow := head + // 快指针 + fast := head + for fast.Next != nil && fast.Next.Next != nil { // 循环结束,slow停在链表中点位置 + slow = slow.Next + fast = fast.Next.Next + } + + fast = slow.Next // 快指针回到中点的下一个节点,之后快指针将从每次走两步,变为每次走一步 + slow.Next = nil // 从中点截断链表,mid.next -> nil + var tmp *Node + // 对原链表右半部分,进行逆序,逆序后,从原尾节点指向中点 + for fast != nil { + tmp = fast.Next // tmp暂时保存fast的下一个节点 + fast.Next = slow // 翻转链表指向 + slow = fast // slow 移动 + fast = tmp // fast 移动 + } + + // tmp指针记录最后的位置,之后把右半部再逆序回原来的次序 + tmp = slow + fast = head + var res = true + for slow != nil && fast != nil { // 原链表的左右部门进行回文对比 + if slow.Val != fast.Val { + res = false + break + } + slow = slow.Next // 从原链表头节点,往原链表中间节点移动 + fast = fast.Next // 从原链表尾节点,往原链表中间节点移动 + } + slow = tmp.Next + tmp.Next = nil + // 把原链表右半部分再逆序回来 + for slow != nil { + fast = slow.Next + slow.Next = tmp + tmp = slow + slow = fast + } + // 返回回文的判断结果 true + return res +} +``` + +### 1.1.3 面试题三 + +> 一种特殊的单链表结构如下: + +```Go +type CopyLinkedListNode struct { + Val int + Next *CopyLinkedListNode + Rand *CopyLinkedListNode +} +``` + +> rand指针是单链表节点结构中新增加的指针,rand可能指向链表中的任意一个节点,也可能为null。给定一个由Node节点类型组成的无环单链表节点head。请实现一个函数完成这个链表的复制,并返回复制的新链表的头结点。 + +> 要求时间复杂度为O(N),额外空间复杂度为O(1) + +1. 哈希表方法(笔试推荐) + +> 第一步遍历,把所有节点加入到Map表示老节点到克隆出来的节点映射 + +> 第二步遍历,查map找到克隆节点,最后返回头结点 + +2. 不用哈希表的方法,人为构造对应关系(面试推荐) + +> 第一步:每个节点遍历的时候克隆出来一个新的节点加入到当前节点和其next节点的中间 + +> 第二步:此时经过第一步所有节点和其克隆节点都是串在一起的,依次拿出当前节点和其克隆节点,当前节点的rand指针指向的节点的克隆节点给当前节点克隆的节点的rand节点指向的节点。 + +> 第三步:此时老节点的rand指针没变化,克隆节点的rand指针也都指向了对应的克隆节点。此时在大的链表上分离出来原链表和克隆链表 + +```Go +package main + +type CopyLinkedListNode struct { + Val int + Next *CopyLinkedListNode + Rand *CopyLinkedListNode +} + +// 方法1 +func (head *CopyLinkedListNode) copyListWithRand1() *CopyLinkedListNode { + m := make(map[*CopyLinkedListNode]*CopyLinkedListNode) + cur := head + for cur != nil { + // 当前节点,克隆出来一个相同值的新节点加入map中 + m[cur] = &CopyLinkedListNode{Val: cur.Val} + cur = cur.Next + } + // 当前节点从头开始 + cur = head + for cur != nil { + // cur原节点,m[cur]是新节点 + m[cur].Next = m[cur.Next] + m[cur].Rand = m[cur.Rand] + cur = cur.Next + } + // 返回原头结点的映射,也就是克隆链表的头结点 + return m[head] +} + +// 方法2 +func (head *CopyLinkedListNode) copyListWithRand2() *CopyLinkedListNode { + if head == nil { + return head + } + cur := head + var next *CopyLinkedListNode = nil + // 克隆出来的node放在原本node和next指向的node中间 + // 1 -> 2 + // 1 -> 1' -> 2 + for cur != nil { + // cur 老节点 next 老的下一个节点 + next = cur.Next + cur.Next = &CopyLinkedListNode{Val: cur.Val} + cur.Next.Next = next + cur = next + } + cur = head + var curCopy *CopyLinkedListNode = nil + // set copy node rand + // 1 -> 1' -> 2 -> 2' + // 设置新的克隆节点间的rand节点 + for cur != nil { + // cur 老节点 + // cur.next => 新的 copy出来的节点 + next = cur.Next.Next + curCopy = cur.Next + if cur.Rand != nil { + curCopy.Rand = cur.Rand.Next + } else { + curCopy.Rand = nil + } + cur = next + } + // 老的头结点:head 新克隆出来的头结点: head.next + res := head.Next + cur = head + // split,分离原本节点组成的链表和克隆节点组成的链表 + for cur != nil { + next = cur.Next.Next + curCopy = cur.Next + cur.Next = next + if next != nil { + curCopy.Next = next.Next + } else { + curCopy.Next = nil + } + cur = next + } + return res +} +``` + +### 1.1.4 面试题四 + +> 该问题和约舍夫环问题是链表问题的比较难的问题 + +> 题目描述:给定两个可能有环也可能无环的单链表,头结点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null + +> 要求:如果两个链表长度之和为N,时间复杂度请达到O(N),额为空间复杂度请达到O(1) + +> 思路:由于是单链表,则一旦成环就结束,出不来,因为每个节点只有一个Next指针 + +1. 方法一:用set把每个节点的内存地址放到set里面,如果存在相同的内存在set中存在,则就是第一个成环的节点 + + +2. 方法二:用快慢指针对链表遍历,那么快慢指针一定会相遇,能相遇就说明存在环。然后让慢指针停在相遇的位置,快指针回到头结点。快指针和慢指针再出发且快指针也变成一次走一步和满指针相同,再次相遇的节点就是成环节点 + +```Go +package main + +import ( + "fmt" + "math" +) + +type Node struct { + Val int + Next *Node +} + +// GetIntersectNode 给定两个链表的头节点,判断两个链表是否相交,如果两个链表相交,请返回相交的第一个节点,不相交则返回nil +func GetIntersectNode(head1, head2 *Node) *Node { + if head1 == nil || head2 == nil { + return nil + } + // head1的第一个入环节点 + loop1 := head1.GetLoopNode() + // head2的第一个入环节点 + loop2 := head2.GetLoopNode() + // 两个无环链表是否相交的情况 + // 由于每个节点只有一个next指针,则如果两个无环相交则相交之后就只剩下公共部分 + // 方法1把第一条链表放到set中,第二个链表依次查在不在该set中,第一个找到的就是 + // 方法2 + // 把链表1走到尾结点end1,记录长度l1 + // 把链表1走到尾结点end2,记录长度l2 + // 如果end1和end2的内存地址不同一定不相交 + // 如果end1==end2,则(1)长的链表从头结点先走保证和短链表相同长度的位置,再以此往下走,第一次相同节点 + // (2)则依次从尾结点出发,找第一次出现内存地址不相同的那个节点,该节点的next节点就是第一次相交的节点 + if loop1 == nil && loop2 == nil { + return NoLoop(head1, head2) + } + // 一个为空,另外一个不为空不可能相交。两个都不为空的情况下共用一个环 + if loop1 != nil && loop2 != nil { + return BothLoop(head1, loop1, head2, loop2) + } + return nil +} + +// GetLoopNode 给定一个链表头节点,找到链表第一个入环节点,如果无环,返回nil +func (head *Node) GetLoopNode() *Node { + // 少于三个节点,无环 + if head == nil || head.Next == nil || head.Next.Next == nil { + return nil + } + idx1 := head.Next // idx1 为慢指针下标 + idx2 := head.Next.Next // idx2 为快指针下标 + for idx1 != idx2 { + if idx2.Next == nil || idx2.Next.Next == nil { + return nil + } + idx2 = idx2.Next.Next + idx1 = idx1.Next + } + // idx1 == idx2 相遇。快指针回到开头,满指针停在原地 + idx2 = head + for idx1 != idx2 { + // 此时快慢指针每次移动相同步数 + idx1 = idx1.Next + idx2 = idx2.Next + } + // 再次相遇的点,即为成环的点 + return idx1 +} + +// NoLoop 给定两个无环链表的头节点, 如果相交返回相交节点,如果不想交返回nil +func NoLoop(head1, head2 *Node) *Node { + if head1 == nil || head2 == nil { + return nil + } + cur1 := head1 + cur2 := head2 + n := 0 + // 遍历链表1,记录节点个数 + for cur1.Next != nil { + n++ + cur1 = cur1.Next + } + // 遍历链表2,记录节点个数 + for cur2.Next != nil { + n-- + cur2 = cur2.Next + } + // 由于是单链表,所以如果两个无环链表相交,必定是从相交的点往后,为公共部分,类似这种形状: + // -------------------------> + // 1 2 3 4 + // 5 7 8 9 6 3 + // 2 5 8 2 4 5 7 + if cur1 != cur2 { // 遍历完两个链表,如果有环进入公共部分,所以cur1和cur2必定地址相同 + return nil + } + // n绝对值此时为 : 链表1长度减去链表2长度的值 + // 谁长,谁的头变成cur1,谁短,谁的头节点变为cur2 + if n > 0 { // 链表1 长 + cur1 = head1 + cur2 = head2 + } else { // 链表2长或两个链表相等 + cur1 = head2 + cur2 = head1 + } + // 取n的绝对值 + n = int(math.Abs(float64(n))) + for n != 0 { // 长链表,先走到差值位置。此时长短链表剩余的长度相等 + n-- + cur1 = cur1.Next + } + + for cur1 != cur2 { // 两个链表共同向下移动,一旦地址相等,即为第一个相交的节点 + cur1 = cur1.Next + cur2 = cur2.Next + } + return cur1 +} + +// BothLoop 两个有环链表,返回第一个相交节点,如果不想交返回nil +// head1的入环节点是loop1,head2的入环节点是loop2 +func BothLoop(head1, loop1, head2, loop2 *Node) *Node { + var cur1 *Node = nil + var cur2 *Node = nil + // 类似第一种都无环的情况 + // 由于是单链表,那么最终成环的是两个链表的公共部门 + // ----------------------------------------> + // 1 2 3 4 + // + // 5 7 8 9 6 3 4 5 7 8 + // 3 9 + // 7 + // 2 5 8 2 4 5 7 + if loop1 == loop2 { // 情况1: 公共部分在成环前 + cur1 = head1 + cur2 = head2 + n := 0 + for cur1 != loop1 { + n++ + cur1 = cur1.Next + } + for cur2 != loop2 { + n-- + cur2 = cur2.Next + } + // 谁长,谁的头变成cur1,谁短,谁的头节点变为cur2 + if n > 0 { // 链表1 长 + cur1 = head1 + cur2 = head2 + } else { // 链表2长或两个链表相等 + cur1 = head2 + cur2 = head1 + } + // 取n的绝对值 + n = int(math.Abs(float64(n))) + for n != 0 { // 长链表,先走到差值位置。此时长短链表剩余的长度相等 + n-- + cur1 = cur1.Next + } + for cur1 != cur2 { // 两个链表共同向下移动,一旦地址相等,即为第一个相交的节点 + cur1 = cur1.Next + cur2 = cur2.Next + } + return cur1 + } else { // 情况2: 公共部门在环内。 + // 找第一个成环节点转回自身的过程中遇到loop2, 则相交,否则不相交 + cur1 = loop1.Next + for cur1 != loop1 { // 链表1沿着成环节点转一圈 + if cur1 == loop2 { + return loop1 + } + cur1 = cur1.Next + } + return nil + } +} + +func main() { + // 1->2->3->4->5->6->7->null + head1 := &Node{Val: 1} + head1.Next = &Node{Val: 2} + head1.Next.Next = &Node{Val: 3} + head1.Next.Next.Next = &Node{Val: 4} + head1.Next.Next.Next.Next = &Node{Val: 5} + head1.Next.Next.Next.Next.Next = &Node{Val: 6} + head1.Next.Next.Next.Next.Next.Next = &Node{Val: 7} + + // 0->9->8->6->7->null + head2 := &Node{Val: 0} + head2.Next = &Node{Val: 9} + head2.Next.Next = &Node{Val: 8} + head2.Next.Next.Next = head1.Next.Next.Next.Next.Next // 8 -> head1(6) + fmt.Println(GetIntersectNode(head1, head2)) + + // 1->2->3->4->5->6->7->4... + head1 = &Node{Val: 1} + head1.Next = &Node{Val: 2} + head1.Next.Next = &Node{Val: 3} + head1.Next.Next.Next = &Node{Val: 4} + head1.Next.Next.Next.Next = &Node{Val: 5} + head1.Next.Next.Next.Next.Next = &Node{Val: 6} + head1.Next.Next.Next.Next.Next.Next = &Node{Val: 7} + head1.Next.Next.Next.Next.Next.Next.Next = head1.Next.Next.Next // 7 -> head1(4) + + // 0->9->8->2... + head2 = &Node{Val: 0} + head2.Next = &Node{Val: 9} + head2.Next.Next = &Node{Val: 8} + head2.Next.Next.Next = head1.Next // 8 -> head1(2) + fmt.Println(GetIntersectNode(head1, head2)) + + // 0->9->8->6->4->5->6.. + head2 = &Node{Val: 0} + head2.Next = &Node{Val: 9} + head2.Next.Next = &Node{Val: 8} + head2.Next.Next.Next = head1.Next.Next.Next.Next.Next // 8 -> head1(6) + fmt.Println(GetIntersectNode(head1, head2)) + +} +``` + +输出: + +```shell +&{6 0xc000010290} +&{2 0xc000010300} +&{4 0xc000010320} + +Process finished with the exit code 0 +``` + diff --git "a/07-\344\272\214\345\217\211\346\240\221\345\237\272\346\234\254\347\256\227\346\263\225.md" "b/07-\344\272\214\345\217\211\346\240\221\345\237\272\346\234\254\347\256\227\346\263\225.md" index 4180440..e05433b 100644 --- "a/07-\344\272\214\345\217\211\346\240\221\345\237\272\346\234\254\347\256\227\346\263\225.md" +++ "b/07-\344\272\214\345\217\211\346\240\221\345\237\272\346\234\254\347\256\227\346\263\225.md" @@ -5,15 +5,15 @@ ### 1.1.1 二叉树节点定义 -```Java - Class Node{ - // 节点的值类型 - V value; - // 二叉树的左孩子指针 - Node left; - // 二叉树的右孩子指针 - Node right; - } +```Go +type Node struct { + // 二叉树节点上的值 + Val int + // 左孩子 + Left *Node + // 右孩子 + Right *Node +} ``` ### 1.1.2 递归实现先序中序后序遍历 @@ -42,229 +42,226 @@ graph TD 3、 后序遍历为:4 5 2 6 7 3 1 -```Java -package class07; - -public class Code01_RecursiveTraversalBT { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int v) { - value = v; - } - } - - public static void f(Node head) { - if (head == null) { - return; - } - // 1 此处打印等于先序 - f(head.left); - // 2 此处打印等于中序 - f(head.right); - // 3 此处打印等于后序 - } +```Go +package main - // 先序打印所有节点 - public static void pre(Node head) { - if (head == null) { - return; - } - // 打印头 - System.out.println(head.value); - // 递归打印左子树 - pre(head.left); - // 递归打印右子树 - pre(head.right); - } +import "fmt" - // 中序遍历 - public static void in(Node head) { - if (head == null) { - return; - } - in(head.left); - System.out.println(head.value); - in(head.right); - } +type Node struct { + // 二叉树节点上的值 + Val int + // 左孩子 + Left *Node + // 右孩子 + Right *Node +} - // 后序遍历 - public static void pos(Node head) { - if (head == null) { - return; - } - pos(head.left); - pos(head.right); - System.out.println(head.value); - } +// Pre 给定二叉树头节点,先序遍历该二叉树 +func (head *Node) Pre() { + if head == nil { + return + } + // 获取头节点,打印该头结点 + fmt.Println(head.Val) + // 递归遍历左子树 + head.Left.Pre() + // 递归遍历右子树 + head.Right.Pre() +} - public static void main(String[] args) { - Node head = new Node(1); - head.left = new Node(2); - head.right = new Node(3); - head.left.left = new Node(4); - head.left.right = new Node(5); - head.right.left = new Node(6); - head.right.right = new Node(7); - - pre(head); - System.out.println("========"); - in(head); - System.out.println("========"); - pos(head); - System.out.println("========"); +// Mid 给定二叉树头节点,中序遍历该二叉树 +func (head *Node) Mid() { + if head == nil { + return + } + // 递归遍历左子树 + head.Left.Mid() + // 获取头节点,打印该头结点 + fmt.Println(head.Val) + // 递归遍历右子树 + head.Right.Mid() +} - } +// Pos 给定二叉树头节点,后序遍历该二叉树 +func (head *Node) Pos() { + if head == nil { + return + } + // 递归遍历左子树 + head.Left.Pos() + // 递归遍历右子树 + head.Right.Pos() + // 获取头节点,打印该头结点 + fmt.Println(head.Val) +} +func main() { + head := &Node{Val: 1} + head.Left = &Node{Val: 2} + head.Right = &Node{Val: 3} + head.Left.Left = &Node{Val: 4} + head.Left.Right = &Node{Val: 5} + head.Right.Left = &Node{Val: 6} + head.Right.Right = &Node{Val: 7} + + head.Pre() + fmt.Println("=========") + head.Mid() + fmt.Println("=========") + head.Pos() } ``` -> 结论:对于树的递归,每个节点实质上会到达三次,例如上文的树结构,对于f函数,我们传入头结点,再调用左树再调用右树。实质上经过的路径为1 2 4 4 4 2 5 5 5 2 1 3 6 6 6 3 7 7 7 3 1。我们在每个节点三次返回的基础上,第一次到达该节点就打印,就是先序,第二次到达该节点打印就是中序,第三次到达该节点就是后序。 - -> 所以先序中序后序,只是我们的递归顺序加工出来的结果! +输出: + +```shell +1 +2 +4 +5 +3 +6 +7 +========= +4 +2 +5 +1 +6 +3 +7 +========= +4 +5 +2 +6 +7 +3 +1 + +Process finished with the exit code 0 -### 1.1.3 非递归实现先序中序后序遍历(DFS) +``` -> 思路:由于任何递归可以改为非递归,我们可以使用压栈来实现,实质就是深度优先遍历(DFS)。用先序实现的步骤,其他类似: +> 总结:对于树的递归,每个节点在递归的过程中实质上会到达三次,例如上文的树结构,我们在第一次到达当前节点就打印,对于以当前节点为树根的树,就是先序。同理,第二次到达当前节点就是中序,第三次到达该节点就是后序遍历。所以先序中序后序,只是我们的递归顺序加工出来的结果而已 -> 步骤一,把节点压入栈中,弹出就打印 +### 1.1.3 非递归实现先序中序后序遍历(DFS) -> 步骤二,如果有右孩子先压入右孩子 +思路:由于任何递归可以改为非递归,我们可以使用压栈来实现,实质就是深度优先遍历(DFS)。用先序实现的步骤,其他类似: -> 步骤三,如果有左孩子压入左孩子 +- 步骤一,把节点压入栈中,弹出就打印 -```Java -package class07; +- 步骤二,如果有右孩子先压入右孩子 -import java.util.Stack; +- 步骤三,如果有左孩子压入左孩子 -public class Code02_UnRecursiveTraversalBT { +```Go +package main - public static class Node { - public int value; - public Node left; - public Node right; +import "fmt" - public Node(int v) { - value = v; - } - } +type Node struct { + // 二叉树节点上的值 + Val int + // 左孩子 + Left *Node + // 右孩子 + Right *Node +} - // 非递归先序 - public static void pre(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.print(head.value + " "); - // 右孩子不为空,压右 - if (head.right != null) { - stack.push(head.right); - } - // 左孩子不为空,压左 - if (head.left != null) { - stack.push(head.left); - } +// Pre 给定二叉树头节点,非递归先序遍历该二叉树 +func (head *Node) Pre() { + fmt.Println("pre-order: ") + if head != nil { + // 简单模拟一个栈 + stack := make([]*Node, 0) + stack = append(stack, head) + for len(stack) != 0 { + // 出栈 + hd := stack[len(stack)-1] + fmt.Println(hd.Val) + stack = stack[:len(stack)-1] + // 右孩子入栈 + if hd.Right != nil { + stack = append(stack, hd.Right) + } + // 左孩子入栈 + if hd.Left != nil { + stack = append(stack, hd.Left) } } - System.out.println(); } + fmt.Println() +} - // 非递归中序 - public static void in(Node head) { - System.out.print("in-order: "); - if (head != null) { - Stack stack = new Stack(); - while (!stack.isEmpty() || head != null) { - // 整条左边界依次入栈 - if (head != null) { - stack.push(head); - head = head.left; - // 左边界到头弹出一个打印,来到该节点右节点,再把该节点的左树以此进栈 - } else { - head = stack.pop(); - System.out.print(head.value + " "); - head = head.right; - } +// Mid 给定二叉树头节点,非递归中序遍历该二叉树 +func (head *Node) Mid() { + fmt.Println("Mid-order:") + if head != nil { + hd := head + // 简单模拟一个栈 + stack := make([]*Node, 0) + for len(stack) != 0 || hd != nil { + // 整条左边界依次入栈 + if hd != nil { + stack = append(stack, hd) + hd = hd.Left + } else { // 左边界到头弹出一个打印,来到该节点右节点,再把该节点的左树以此进栈 + hd = stack[len(stack)-1] + stack = stack[:len(stack)-1] + fmt.Println(hd.Val) + hd = hd.Right } } - System.out.println(); } + fmt.Println() +} - // 非递归后序 - public static void pos1(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); - } +// Pos 给定二叉树头节点,非递归后序遍历该二叉树 +func (head *Node) Pos() { + fmt.Println("pos-order: ") + if head != nil { + hd := head + // 借助两个辅助栈 + s1 := make([]*Node, 0) + s2 := make([]*Node, 0) + s1 = append(s1, hd) + for len(s1) != 0 { + // 出栈 + hd = s1[len(s1) - 1] + s1 = s1[:len(s1) - 1] + s2 = append(s2, hd) + if hd.Left != nil { + s1 = append(s1, hd.Left) } - while (!s2.isEmpty()) { - System.out.print(s2.pop().value + " "); + if hd.Right != nil { + s1 = append(s1, hd.Right) } } - System.out.println(); - } - - // 非递归后序2:用一个栈实现后序遍历,比较有技巧 - public static void pos2(Node h) { - System.out.print("pos-order: "); - if (h != null) { - Stack stack = new Stack(); - stack.push(h); - Node c = null; - while (!stack.isEmpty()) { - c = stack.peek(); - if (c.left != null && h != c.left && h != c.right) { - stack.push(c.left); - } else if (c.right != null && h != c.right) { - stack.push(c.right); - } else { - System.out.print(stack.pop().value + " "); - h = c; - } - } + for len(s2) != 0 { + v := s2[len(s2) - 1] + s2 = s2[:len(s2) - 1] + fmt.Println(v.Val) } - System.out.println(); } - - public static void main(String[] args) { - Node head = new Node(1); - head.left = new Node(2); - head.right = new Node(3); - head.left.left = new Node(4); - head.left.right = new Node(5); - head.right.left = new Node(6); - head.right.right = new Node(7); - - pre(head); - System.out.println("========"); - in(head); - System.out.println("========"); - pos1(head); - System.out.println("========"); - pos2(head); - System.out.println("========"); - } - + fmt.Println() } +func main() { + head := &Node{Val: 1} + head.Left = &Node{Val: 2} + head.Right = &Node{Val: 3} + head.Left.Left = &Node{Val: 4} + head.Left.Right = &Node{Val: 5} + head.Right.Left = &Node{Val: 6} + head.Right.Right = &Node{Val: 7} + + head.Pre() + fmt.Println("=========") + head.Mid() + fmt.Println("=========") + head.Pos() +} ``` ### 1.1.4 二叉树按层遍历(BFS) @@ -275,204 +272,170 @@ public class Code02_UnRecursiveTraversalBT { > 按层打印输出二叉树 -```Java -package class07; - -import java.util.LinkedList; -import java.util.Queue; - -public class Code03_LevelTraversalBT { +```GO +package main - public static class Node { - public int value; - public Node left; - public Node right; +import "fmt" - public Node(int v) { - value = v; - } - } +type Node struct { + // 二叉树节点上的值 + Val int + // 左孩子 + Left *Node + // 右孩子 + Right *Node +} - 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); - } +// Level 按层遍历二叉树 +func (head *Node) Level() { + if head == nil { + return + } + hd := head + // 简单实现一个队列 + queue := make([]*Node, 0) + // 加入头结点 + queue = append(queue, hd) + // 队列不为空出队打印,把当前节点的左右孩子加入队列 + for len(queue) != 0 { + // 弹出队列头部的元素 + cur := queue[0] + queue = queue[1:] + fmt.Println(cur.Val) + if cur.Left != nil { + queue = append(queue, cur.Left) + } + if cur.Right != nil { + queue = append(queue, cur.Right) } } +} - public static void main(String[] args) { - Node head = new Node(1); - head.left = new Node(2); - head.right = new Node(3); - head.left.left = new Node(4); - head.left.right = new Node(5); - head.right.left = new Node(6); - head.right.right = new Node(7); - - level(head); - System.out.println("========"); - } +func main() { + head := &Node{Val: 1} + head.Left = &Node{Val: 2} + head.Right = &Node{Val: 3} + head.Left.Left = &Node{Val: 4} + head.Left.Right = &Node{Val: 5} + head.Right.Left = &Node{Val: 6} + head.Right.Right = &Node{Val: 7} + head.Level() } - ``` > 找到二叉树的最大宽度 -```Java -package class07; +```Go +package main -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Queue; +import "math" -public class Code06_TreeMaxWidth { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - // 方法1使用map - public static int maxWidthUseMap(Node head) { - if (head == null) { - return 0; - } - Queue queue = new LinkedList<>(); - queue.add(head); - // key(节点) 在 哪一层,value - HashMap levelMap = new HashMap<>(); - // head在第一层 - levelMap.put(head, 1); - // 当前你正在统计哪一层的宽度 - int curLevel = 1; - // 当前层curLevel层,宽度目前是多少 - int curLevelNodes = 0; - // 用来保存所有层的最大值,也就是最大宽度 - int max = 0; - while (!queue.isEmpty()) { - Node cur = queue.poll(); - int curNodeLevel = levelMap.get(cur); - // 当前节点的左孩子不为空,队列加入左孩子,层数在之前层上加1 - if (cur.left != null) { - levelMap.put(cur.left, curNodeLevel + 1); - queue.add(cur.left); - } - // 当前节点的右孩子不为空,队列加入右孩子,层数也变为当前节点的层数加1 - if (cur.right != null) { - levelMap.put(cur.right, curNodeLevel + 1); - queue.add(cur.right); - } - // 当前层等于正在统计的层数,不结算 - if (curNodeLevel == curLevel) { - curLevelNodes++; - } else { - // 新的一层,需要结算 - // 得到目前为止的最大宽度 - max = Math.max(max, curLevelNodes); - curLevel++; - // 结算后,当前层节点数设置为1 - curLevelNodes = 1; - } - } - // 由于最后一层,没有新的一层去结算,所以这里单独结算最后一层 - max = Math.max(max, curLevelNodes); - return max; - } - - // 方法2不使用map - public static int maxWidthNoMap(Node head) { - if (head == null) { - return 0; - } - Queue queue = new LinkedList<>(); - queue.add(head); - // 当前层,最右节点是谁,初始head的就是本身 - Node curEnd = head; - // 如果有下一层,下一层最右节点是谁 - Node nextEnd = null; - // 全局最大宽度 - int max = 0; - // 当前层的节点数 - int curLevelNodes = 0; - while (!queue.isEmpty()) { - Node cur = queue.poll(); - // 左边不等于空,加入左 - if (cur.left != null) { - queue.add(cur.left); - // 孩子的最右节点暂时为左节点 - nextEnd = cur.left; - } - // 右边不等于空,加入右 - if (cur.right != null) { - queue.add(cur.right); - // 如果有右节点,孩子层的最右要更新为右节点 - nextEnd = cur.right; - } - // 由于最开始弹出当前节点,那么该层的节点数加一 - curLevelNodes++; - // 当前节点是当前层最右的节点,进行结算 - if (cur == curEnd) { - // 当前层的节点和max进行比较,计算当前最大的max - max = Math.max(max, curLevelNodes); - // 即将进入下一层,重置下一层节点为0个节点 - curLevelNodes = 0; - // 当前层的最右,直接更新为找出来的下一层最右 - curEnd = nextEnd; - } - } - return max; - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } +type Node struct { + // 二叉树节点上的值 + Val int + // 左孩子 + Left *Node + // 右孩子 + Right *Node +} - public static void main(String[] args) { - int maxLevel = 10; - int maxValue = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - if (maxWidthUseMap(head) != maxWidthNoMap(head)) { - System.out.println("Oops!"); - } +// MaxWidthUseMap 给定二叉树头节点,找到该二叉树的最大宽度,借助map结构实现 +func (head *Node) MaxWidthUseMap() int { + if head == nil { + return 0 + } + hd := head + queue := make([]*Node, 0) + queue = append(queue, hd) + + // map的Key:节点 map的value:节点属于哪一层 + levelMap := make(map[*Node]int, 0) + // 头节点head属于第一层 + levelMap[hd] = 1 + // 当前正在统计那一层的宽度 + curLevel := 1 + // 当前curLevel层,宽度目前是多少 + curLevelNodes := 0 + // 用来保存所有层的最大宽度的值 + max := 0 + for len(queue) != 0 { + cur := queue[0] + queue = queue[1:] + curNodeLevel := levelMap[cur] + // 当前节点的左孩子不为空,队列加入左孩子,层数在之前层上加1 + if cur.Left != nil { + levelMap[cur.Left] = curNodeLevel + 1 + queue = append(queue, cur.Left) + } + // 当前节点的右孩子不为空,队列加入右孩子,层数也变为当前节点的层数加1 + if cur.Right != nil { + levelMap[cur.Right] = curNodeLevel + 1 + queue = append(queue, cur.Right) + } + // 当前层等于正在统计的层数,不结算 + if curNodeLevel == curLevel { + curLevelNodes ++ + } else { + // 新的一层,需要结算 + // 得到目前为止的最大宽度 + max = int(math.Max(float64(max), float64(curLevelNodes))) + curLevel++ + // 结算后,当前层节点数设置为1 + curLevelNodes = 1 } - System.out.println("finish!"); - } + // 由于最后一层,没有新的一层去结算,所以这里单独结算最后一层 + max = int(math.Max(float64(max), float64(curLevelNodes))) + return max +} +// MaxWidthNoMap 给定二叉树头节点,找到该二叉树的最大宽度,不借助map实现 +func (head *Node) MaxWidthNoMap() int { + if head == nil { + return 0 + } + + hd := head + queue := make([]*Node, 0) + queue = append(queue, hd) + + // 当前层,最右节点是谁,初始head的就是本身 + curEnd := head + // 如果有下一层,下一层最右节点是谁 + var nextEnd *Node = nil + // 全局最大宽度 + max := 0 + // 当前层的节点数 + curLevelNodes := 0 + for len(queue) != 0 { + cur := queue[0] + queue = queue[1:] + // 左边不等于空,加入左 + if cur.Left != nil { + queue = append(queue, cur.Left) + // 孩子的最右节点暂时为左节点 + nextEnd = cur.Left + } + // 右边不等于空,加入右 + if cur.Right != nil { + queue = append(queue, cur.Right) + // 如果有右节点,孩子层的最右要更新为右节点 + nextEnd = cur.Right + } + // 由于最开始弹出当前节点,那么该层的节点数加一 + curLevelNodes++ + // 当前节点是当前层最右的节点,进行结算 + if cur == curEnd { + // 当前层的节点和max进行比较,计算当前最大的max + max = int(math.Max(float64(max), float64(curLevelNodes))) + // 即将进入下一层,重置下一层节点为0个节点 + curLevelNodes = 0 + // 当前层的最右,直接更新为找出来的下一层最右 + curEnd = nextEnd + } + } + return max } ``` @@ -480,411 +443,95 @@ public class Code06_TreeMaxWidth { 1、 可以用先序或者中序或者后序或者按层遍历,来实现二叉树的序列化 -2、 用了什么方式的序列化,就用什么方式的反序列化 +2、 用了什么方式的序列化,就用什么方式的反序列化。后续序列化和按层序列化略,感兴趣可以自己查资料 > 由于如果树上的节点值相同,那么序列化看不出来该树的结构,所以我们的序列化要加上空间结构的标识,空节点补全的方式。 -```Java -package class07; - -import java.util.LinkedList; -import java.util.Queue; -import java.util.Stack; - -public class Code04_SerializeAndReconstructTree { - /* - * 二叉树可以通过先序、后序或者按层遍历的方式序列化和反序列化, - * 以下代码全部实现了。 - * 但是,二叉树无法通过中序遍历的方式实现序列化和反序列化 - * 因为不同的两棵树,可能得到同样的中序序列,即便补了空位置也可能一样。 - * 比如如下两棵树 - * __2 - * / - * 1 - * 和 - * 1__ - * \ - * 2 - * 补足空位置的中序遍历结果都是{ null, 1, null, 2, null} - * - * */ - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - // 先序序列化 - public static Queue preSerial(Node head) { - Queue ans = new LinkedList<>(); - // 先序的序列化结果依次放入队列中去 - pres(head, ans); - return ans; - } - - public 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); - } - } +```Go +package main - // 中序有问题。见文件开头注释 - public static Queue inSerial(Node head) { - Queue ans = new LinkedList<>(); - ins(head, ans); - return ans; - } - - public static void ins(Node head, Queue ans) { - if (head == null) { - ans.add(null); - } else { - ins(head.left, ans); - ans.add(String.valueOf(head.value)); - ins(head.right, ans); - } - } - - // 后序序列化 - public static Queue posSerial(Node head) { - Queue ans = new LinkedList<>(); - poss(head, ans); - return ans; - } - - public static void poss(Node head, Queue ans) { - if (head == null) { - ans.add(null); - } else { - poss(head.left, ans); - poss(head.right, ans); - ans.add(String.valueOf(head.value)); - } - } +import "strconv" - // 根据先序的结构,构建这颗树 - 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; - } - - // 根据后序的结构,构建该树 - public static Node buildByPosQueue(Queue poslist) { - if (poslist == null || poslist.size() == 0) { - return null; - } - // 左右中 -> stack(中右左) - Stack stack = new Stack<>(); - while (!poslist.isEmpty()) { - stack.push(poslist.poll()); - } - return posb(stack); - } - - public static Node posb(Stack posstack) { - String value = posstack.pop(); - if (value == null) { - return null; - } - Node head = new Node(Integer.valueOf(value)); - head.right = posb(posstack); - head.left = posb(posstack); - return head; - } - - // 按层序列化,整体上就是宽度优先遍历 - public static Queue levelSerial(Node head) { - // 序列化结果 - Queue ans = new LinkedList<>(); - if (head == null) { - ans.add(null); - } else { - // 加入一个节点的时候,把该节点的值加入 - ans.add(String.valueOf(head.value)); - // 辅助队列 - Queue queue = new LinkedList(); - queue.add(head); - while (!queue.isEmpty()) { - head = queue.poll(); - // 左孩子不为空,即序列化,也加入队列 - if (head.left != null) { - ans.add(String.valueOf(head.left.value)); - queue.add(head.left); - // 左孩子等于空,只序列化,不加入队列 - } else { - ans.add(null); - } - if (head.right != null) { - ans.add(String.valueOf(head.right.value)); - queue.add(head.right); - } else { - ans.add(null); - } - } - } - return ans; - } - - // 按层反序列化 - public static Node buildByLevelQueue(Queue levelList) { - if (levelList == null || levelList.size() == 0) { - return null; - } - Node head = generateNode(levelList.poll()); - Queue queue = new LinkedList(); - if (head != null) { - queue.add(head); - } - Node node = null; - while (!queue.isEmpty()) { - node = queue.poll(); - // 不管左右孩子是否为空,都要加节点 - node.left = generateNode(levelList.poll()); - node.right = generateNode(levelList.poll()); - // 左孩子不为空,队列加左,为建下一层做准备 - if (node.left != null) { - queue.add(node.left); - } - // 右孩子不为空,队列加右,为建下一层做准备 - if (node.right != null) { - queue.add(node.right); - } - } - return head; - } - - public static Node generateNode(String val) { - if (val == null) { - return null; - } - return new Node(Integer.valueOf(val)); - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } - - // for test - public static boolean isSameValueStructure(Node head1, Node head2) { - if (head1 == null && head2 != null) { - return false; - } - if (head1 != null && head2 == null) { - return false; - } - if (head1 == null && head2 == null) { - return true; - } - if (head1.value != head2.value) { - return false; - } - return isSameValueStructure(head1.left, head2.left) && isSameValueStructure(head1.right, head2.right); - } - - // for test - public static void printTree(Node head) { - System.out.println("Binary Tree:"); - printInOrder(head, 0, "H", 17); - System.out.println(); - } - - public static void printInOrder(Node head, int height, String to, int len) { - if (head == null) { - return; - } - 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); - } +type Node struct { + // 二叉树节点上的值 + Val int + // 左孩子 + Left *Node + // 右孩子 + Right *Node +} - 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(); - } +// PreSerial 二叉树的先序列化 +func (head *Node) PreSerial() []string { + // 简单实现一个队列 + ans := make([]string, 0) + // 先序的序列化结果依次放入队列中去 + pres(head, ans) + return ans +} - public static void main(String[] args) { - int maxLevel = 5; - int maxValue = 100; - int testTimes = 1000000; - System.out.println("test begin"); - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - Queue pre = preSerial(head); - Queue pos = posSerial(head); - Queue level = levelSerial(head); - Node preBuild = buildByPreQueue(pre); - Node posBuild = buildByPosQueue(pos); - Node levelBuild = buildByLevelQueue(level); - if (!isSameValueStructure(preBuild, posBuild) || !isSameValueStructure(posBuild, levelBuild)) { - System.out.println("Oops!"); - } - } - System.out.println("test finish!"); - +func pres(head *Node, ans []string) { + if head == nil { + ans = append(ans, "") + } else { + ans = append(ans, strconv.Itoa(head.Val)) + pres(head.Left, ans) + pres(head.Right, ans) } } -``` - -## 1.3 直观打印一颗二叉树 -> 如何设计一个打印整颗数的打印函数,简单起见,我们躺着打印,正常的树我们顺时针旋转90°即可 - -```Java -package class07; -public class Code05_PrintBinaryTree { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } +// BuildByPreQueue 根据先序序列化的结果,重新构建该二叉树。反序列化 +func BuildByPreQueue(preQueue []string) *Node { + if len(preQueue) == 0 { + return nil } - public static void printTree(Node head) { - System.out.println("Binary Tree:"); - // 打印函数,先传入头结点 - printInOrder(head, 0, "H", 17); - System.out.println(); - } + return preb(preQueue) +} - // head表示当前传入节点 - // height当前节点所在的高度 - // to表示当前节点的指向信息 - // len表示打印当前值填充到多少位当成一个完整的值 - public static void printInOrder(Node head, int height, String to, int len) { - if (head == null) { - return; - } - // 递归右树,右树向下指 - printInOrder(head.right, height + 1, "v", len); - /** - * 打印自己的值 - * val 表示值内容 - **/ - String val = to + head.value + to; - int lenM = val.length(); - // 按照len算该值左侧需要填充多少空格 - int lenL = (len - lenM) / 2; - // 按照len算该值右侧需要填充多少空格 - int lenR = len - lenM - lenL; - // 实际值加上左右占位,表示每个值包括占位之后大小 - val = getSpace(lenL) + val + getSpace(lenR); - System.out.println(getSpace(height * len) + val); - // 递归左树,左树向上指 - printInOrder(head.left, height + 1, "^", len); - } +func preb(preQueue []string) *Node { + v := preQueue[0] + preQueue = preQueue[1:] - // 根据height*len补空格 - 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(); + // 如果头节点是空的话,返回空 + if v == "" { + return nil } - public static void main(String[] args) { - Node head = new Node(1); - head.left = new Node(-222222222); - head.right = new Node(3); - head.left.left = new Node(Integer.MIN_VALUE); - head.right.left = new Node(55555555); - head.right.right = new Node(66); - head.left.left.right = new Node(777); - printTree(head); - - head = new Node(1); - head.left = new Node(2); - head.right = new Node(3); - head.left.left = new Node(4); - head.right.left = new Node(5); - head.right.right = new Node(6); - head.left.left.right = new Node(7); - printTree(head); - - head = new Node(1); - head.left = new Node(1); - head.right = new Node(1); - head.left.left = new Node(1); - head.right.left = new Node(1); - head.right.right = new Node(1); - head.left.left.right = new Node(1); - printTree(head); - + // 否则根据第一个值构建先序的头结点 + if iv, err := strconv.Atoi(v); err == nil { + head := &Node{Val: iv} + // 递归建立左树 + head.Left = preb(preQueue) + // 递归建立右树 + head.Right = preb(preQueue) + return head } - + return nil } - ``` -## 1.4 题目实战 +## 1.3 题目实战 -### 1.4.1 题目一:返回二叉树的后继节点 +### 1.3.1 题目一:返回二叉树的后继节点 题目描述:二叉树的结构定义如下: -```Java -Class Node { - V value; - Node left; - Node right; - // 指向父亲节点 - Node parent; +```Go +type Node struct { + // 二叉树节点上的值 + Val int + // 左孩子 + Left *Node + // 右孩子 + Right *Node + // 指向父亲的指针 + Parent *Node } ``` -给你二叉树中的某个节点,返回该节点的后继节点。后继节点表示一颗二叉树中,在中序遍历的序列中,一个个节点的下一个节点是谁。 +给你二叉树中的某个节点,返回该节点的后继节点。后继节点表示一颗二叉树中,在**中序遍历**的序列中,一个节点的下一个节点是哪个。 > 方法一,通常解法思路:由于我们的节点有指向父节点的指针,而整颗二叉树的头结点的父节点为null。那么我们可以找到整棵树的头结点,然后中序遍历,再找到给定节点的下一个节点,就是该节点的后续节点。 @@ -899,149 +546,90 @@ Class Node { > 如果找父节点,一直找到null都不满足,那么该节点是整棵树的最右节点,没有后继 -```Java -package class07; +```Go +package main -public class Code07_SuccessorNode { +import "fmt" - public static class Node { - public int value; - public Node left; - public Node right; - public Node parent; +type Node struct { + // 二叉树节点上的值 + Val int + // 左孩子 + Left *Node + // 右孩子 + Right *Node + // 指向父亲的指针 + Parent *Node +} - public Node(int data) { - this.value = data; - } +// GetSuccessorNode 给定二叉树的一个节点node,返回该节点的后继节点 +func GetSuccessorNode(node *Node) *Node { + if node == nil { + return node } - // 给定节点,返回后继 - public static Node getSuccessorNode(Node node) { - if (node == null) { - return node; - } - if (node.right != null) { - return getLeftMost(node.right); - // 无右子树 - } else { - Node parent = node.parent; - // 当前节点是其父亲节点右孩子,继续 - while (parent != null && parent.right == node) { - node = parent; - parent = node.parent; - } - return parent; + if node.Right != nil { + return getLeftMost(node.Right) + } else { // 无右子树 + parent := node.Parent + // 当前节点是其父亲节点右孩子,继续 + for parent != nil && parent.Right == node { + node = parent + parent = node.Parent } + return parent } +} - // 找右树上的最左节点 - public static Node getLeftMost(Node node) { - if (node == null) { - return node; - } - while (node.left != null) { - node = node.left; - } - return node; +// 找右树上的最左节点 +func getLeftMost(node *Node) *Node { + if node == nil { + return node } - - public static void main(String[] args) { - Node head = new Node(6); - head.parent = null; - head.left = new Node(3); - head.left.parent = head; - head.left.left = new Node(1); - head.left.left.parent = head.left; - head.left.left.right = new Node(2); - head.left.left.right.parent = head.left.left; - head.left.right = new Node(4); - head.left.right.parent = head.left; - head.left.right.right = new Node(5); - head.left.right.right.parent = head.left.right; - head.right = new Node(9); - head.right.parent = head; - head.right.left = new Node(8); - head.right.left.parent = head.right; - head.right.left.left = new Node(7); - head.right.left.left.parent = head.right.left; - head.right.right = new Node(10); - head.right.right.parent = head.right; - - Node test = head.left.left; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head.left.left.right; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head.left; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head.left.right; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head.left.right.right; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head.right.left.left; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head.right.left; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head.right; - System.out.println(test.value + " next: " + getSuccessorNode(test).value); - test = head.right.right; // 10's next is null - System.out.println(test.value + " next: " + getSuccessorNode(test)); + for node.Left != nil { + node = node.Left } - + return node } -``` - -> 后继节点对应的是前驱结点,前驱结点的含义是中序遍历,某节点的前一个节点 - -### 1.4.2 题目二:折纸问题 - -请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。 - -此时折痕是凹下去的,即折痕凸起的方向指向纸条的背面。 - -如果从纸条的下边向上方对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕,下折痕和上折痕。 - -给定一个输入参数N,代表纸条都从下边向上方连续对折N次。请从上到下打印所有的折痕的方向。 - -例如:N=1时,打印: down 。N=2时,打印:down down up -> 规律,大于一次后,每次折痕出现的位置都是在上次折痕的上方出现凹折痕,下方出现凸折痕。所以我们没必要构建这颗树,就可以用递归思维解决 - -```text -对应的树结构按层输出为: - 1凹 - 2凹 2凸 -3凹 3凸 3凹 3凸 +func main() { + head := &Node{Val: 6} + head.Parent = nil + head.Left = &Node{Val: 3} + head.Left.Parent = head + head.Left.Left = &Node{Val: 1} + head.Left.Left.Parent = head.Left + head.Left.Left.Right = &Node{Val: 2} + head.Left.Left.Right.Parent = head.Left.Left + head.Left.Right = &Node{Val: 4} + head.Left.Right.Parent = head.Left + head.Left.Right.Right = &Node{Val: 5} + head.Left.Right.Right.Parent = head.Left.Right + head.Right = &Node{Val: 9} + head.Right.Parent = head + head.Right.Left = &Node{Val: 8} + head.Right.Left.Parent = head.Right + head.Right.Left.Left = &Node{Val: 7} + head.Right.Left.Left.Parent = head.Right.Left + head.Right.Right = &Node{Val: 10} + head.Right.Right.Parent = head.Right + + + test := head.Left.Left + fmt.Println(fmt.Sprintf("节点:%d的后继节点为%d", test.Val, GetSuccessorNode(test).Val)) + + test = head.Left.Left.Right + fmt.Println(fmt.Sprintf("节点:%d的后继节点为%d", test.Val, GetSuccessorNode(test).Val)) + + test = head.Left.Right.Left + fmt.Println(fmt.Sprintf("节点:%d的后继节点为%d", test.Val, GetSuccessorNode(test).Val)) + + test = head + fmt.Println(fmt.Sprintf("节点:%d的后继节点为%d", test.Val, GetSuccessorNode(test).Val)) + + test = head.Right.Left.Left + fmt.Println(fmt.Sprintf("节点:%d的后继节点为%d", test.Val, GetSuccessorNode(test).Val)) +} ``` -```Java -package class07; - -public class Code08_PaperFolding { - - public static void printAllFolds(int N) { - // 先从头结点出发,i初始值为1,切第一次的头结点折痕为凹折痕 - printProcess(1, N, true); - } - - // 递归过程,来到了某一个节点, - // i是节点的层数,N一共的层数,down == true 凹 down == false 凸 - public static void printProcess(int i, int N, boolean down) { - if (i > N) { - return; - } - // 每个当前节点的左子节点是凹 - printProcess(i + 1, N, true); - System.out.println(down ? "凹 " : "凸 "); - // 每个当前节点的右子树是凸 - printProcess(i + 1, N, false); - } - - public static void main(String[] args) { - int N = 3; - // 折N次,打印所有凹凸分布情况 - printAllFolds(N); - } -} -``` \ No newline at end of file +> 后继节点对应的是前驱结点,前驱结点的含义是中序遍历,某节点的前一个节点 \ No newline at end of file diff --git "a/08-\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\346\200\235\347\273\264.md" "b/08-\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\346\200\235\347\273\264.md" deleted file mode 100644 index 56729ed..0000000 --- "a/08-\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\346\200\235\347\273\264.md" +++ /dev/null @@ -1,1319 +0,0 @@ -[TOC] -# 1 二叉树的递归套路 - -1、 可以解决面试中的绝大部分二叉树(95%以上)的问题,尤其是树形dp问题 - -2、 其本质是利用递归遍历二叉树的便利性,每个节点在递归的过程中可以回到该节点3次 - - -==具体步骤为:== - -1. 假设以X节点为头,假设可以向X左树和右树要任何信息 -2. 在上一步的假设下,讨论以X为头结点的树,得到答案的可能性(最重要),常见分类是与X无关的答案,与X有关的答案 -3. 列出所有可能性后,确定到底需要向左树和右树要什么样的信息 -4. 把左树信息和右树信息求全集,就是任何一颗子树都需要返回的信息S -5. 递归函数都返回S,每颗子树都这么要求 -6. 写代码,在代码中考虑如何把左树信息和右树信息整合出整棵树的信息 - -## 1.1 二叉树的递归套路深度实践 - -### 1.1.1 例一:判断二叉树平衡与否 - -给定一棵二叉树的头结点head,返回这颗二叉树是不是平衡二叉树 - -> 平衡树概念:在一棵二叉树中,每一个子树,左树的高度和右树的高度差不超过1 - -> 那么如果以X为头的这颗树,要做到平衡,那么X的左树要是平衡的,右树也是平衡的,且X的左树高度和右树高度差不超过1 - -> 所以该题,我们X需要向左右子树要的信息为,1.高度 2. 是否平衡 - -```Java -package class08; - -public class Code01_IsBalanced { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - public static boolean isBalanced1(Node head) { - boolean[] ans = new boolean[1]; - ans[0] = true; - process1(head, ans); - return ans[0]; - } - - public static int process1(Node head, boolean[] ans) { - if (!ans[0] || head == null) { - return -1; - } - int leftHeight = process1(head.left, ans); - int rightHeight = process1(head.right, ans); - if (Math.abs(leftHeight - rightHeight) > 1) { - ans[0] = false; - } - return Math.max(leftHeight, rightHeight) + 1; - } - - public static boolean isBalanced2(Node head) { - return process2(head).isBalaced; - } - - // 左、右要求一样,Info 表示信息返回的结构体 - public static class Info { - // 是否平衡 - public boolean isBalaced; - // 高度多少 - public int height; - - public Info(boolean b, int h) { - isBalaced = b; - height = h; - } - } - - // 递归调用,X自身也要返回信息Info。 - // 解决X节点(当前节点)怎么返回Info信息 - public static Info process2(Node X) { - // base case - if (X == null) { - return new Info(true, 0); - } - // 得到左树信息 - Info leftInfo = process2(X.left); - // 得到右树信息 - Info rightInfo = process2(X.right); - - // 高度等于左右最大高度,加上当前头结点的高度1 - int height = Math.max(leftInfo.height, rightInfo.height) + 1; - boolean isBalanced = true; - // 左树不平衡或者右树不平衡,或者左右两子树高度差超过1 - // 那么当前节点为头的树,不平衡 - if (!leftInfo.isBalaced || !rightInfo.isBalaced || Math.abs(leftInfo.height - rightInfo.height) > 1) { - isBalanced = false; - } - // 加工出当前节点的信息返回 - return new Info(isBalanced, height); - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } - - public static void main(String[] args) { - int maxLevel = 5; - int maxValue = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - if (isBalanced1(head) != isBalanced2(head)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} - -``` - -### .1.2 例二:返回二叉树任意两个节点最大值 - -给定一棵二叉树的头结点head,任何两个节点之间都存在距离,返回整棵二叉树的最大距离 - -> 1、有可能最大距离和当前节点X无关,即最大距离是X左树的最大距离,或者右树的最大距离 - -> 2、最大距离跟X有关,即最大距离通过X。左树离X最远的点,到X右树上离X最远的点。即X左树的高度加上X自身高度1,加上X右树上的高度 - -> 结论:那么根据递归套路,我们每次递归,需要返回X左树的最大距离和高度,同理返回X右树的最大距离和高度。Info包含最大距离和高度 - -```Java -package class08; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; - -public class Code08_MaxDistance { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - public static int maxDistance1(Node head) { - if (head == null) { - return 0; - } - ArrayList arr = getPrelist(head); - HashMap parentMap = getParentMap(head); - int max = 0; - for (int i = 0; i < arr.size(); i++) { - for (int j = i; j < arr.size(); j++) { - max = Math.max(max, distance(parentMap, arr.get(i), arr.get(j))); - } - } - return max; - } - - public static ArrayList getPrelist(Node head) { - ArrayList arr = new ArrayList<>(); - fillPrelist(head, arr); - return arr; - } - - public static void fillPrelist(Node head, ArrayList arr) { - if (head == null) { - return; - } - arr.add(head); - fillPrelist(head.left, arr); - fillPrelist(head.right, arr); - } - - public static HashMap getParentMap(Node head) { - HashMap map = new HashMap<>(); - map.put(head, null); - fillParentMap(head, map); - return map; - } - - public static void fillParentMap(Node head, HashMap parentMap) { - if (head.left != null) { - parentMap.put(head.left, head); - fillParentMap(head.left, parentMap); - } - if (head.right != null) { - parentMap.put(head.right, head); - fillParentMap(head.right, parentMap); - } - } - - public static int distance(HashMap parentMap, Node o1, Node o2) { - HashSet o1Set = new HashSet<>(); - Node cur = o1; - o1Set.add(cur); - while (parentMap.get(cur) != null) { - cur = parentMap.get(cur); - o1Set.add(cur); - } - cur = o2; - while (!o1Set.contains(cur)) { - cur = parentMap.get(cur); - } - Node lowestAncestor = cur; - cur = o1; - int distance1 = 1; - while (cur != lowestAncestor) { - cur = parentMap.get(cur); - distance1++; - } - cur = o2; - int distance2 = 1; - while (cur != lowestAncestor) { - cur = parentMap.get(cur); - distance2++; - } - return distance1 + distance2 - 1; - } - - public static int maxDistance2(Node head) { - return process(head).maxDistance; - } - - // 我们的信息,整棵树的最大距离和整棵树的高度 - public static class Info { - public int maxDistance; - public int height; - - public Info(int dis, int h) { - maxDistance = dis; - height = h; - } - } - - // 以X节点为头 - public static Info process(Node X) { - // base case - if (X == null) { - return new Info(0, 0); - } - // 默认从左树拿到我们需要的info - Info leftInfo = process(X.left); - // 默认从右树拿到我们需要的info - Info rightInfo = process(X.right); - // 用左右树的信息,加工自身的info - // 自身的高度是,左右较大的高度加上自身节点高度1 - int height = Math.max(leftInfo.height, rightInfo.height) + 1; - // 自身最大距离,是左右树最大距离和左右树高度相加再加1,求最大值 - int maxDistance = Math.max( - Math.max(leftInfo.maxDistance, rightInfo.maxDistance), - leftInfo.height + rightInfo.height + 1); - // 自身的info返回 - return new Info(maxDistance, height); - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } - - public static void main(String[] args) { - int maxLevel = 4; - int maxValue = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - if (maxDistance1(head) != maxDistance2(head)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} -``` - -### 1.1.3 例三:返回二叉树中的最大二叉搜索树Size - -给定一颗二叉树的头结点head,返回这颗二叉树中最大的二叉搜索树的Size - -> 搜索二叉树概念:整颗树上没有重复值,左树的值都比我小,右树的值都比我大。每颗子树都如此。 - -> 递归套路。1、与当前节点X无关,即最终找到的搜索二叉树,不以X为头 - -> 2、与X有关,那么X的左树整体是搜索二叉树,右树同理,且左树的最大值小于X,右树的最小值大于X - -```Java -package class08; - -import java.util.ArrayList; - -public class Code04_MaxSubBSTSize { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - public static int getBSTSize(Node head) { - if (head == null) { - return 0; - } - ArrayList arr = new ArrayList<>(); - in(head, arr); - for (int i = 1; i < arr.size(); i++) { - if (arr.get(i).value <= arr.get(i - 1).value) { - return 0; - } - } - return arr.size(); - } - - public static void in(Node head, ArrayList arr) { - if (head == null) { - return; - } - in(head.left, arr); - arr.add(head); - in(head.right, arr); - } - - public static int maxSubBSTSize1(Node head) { - if (head == null) { - return 0; - } - int h = getBSTSize(head); - if (h != 0) { - return h; - } - return Math.max(maxSubBSTSize1(head.left), maxSubBSTSize1(head.right)); - } - - public static int maxSubBSTSize2(Node head) { - if (head == null) { - return 0; - } - return process(head).maxSubBSTSize; - } - -// public static Info process(Node head) { -// if (head == null) { -// return null; -// } -// Info leftInfo = process(head.left); -// Info rightInfo = process(head.right); -// int min = head.value; -// int max = head.value; -// int maxSubBSTSize = 0; -// if (leftInfo != null) { -// min = Math.min(min, leftInfo.min); -// max = Math.max(max, leftInfo.max); -// maxSubBSTSize = Math.max(maxSubBSTSize, leftInfo.maxSubBSTSize); -// } -// if (rightInfo != null) { -// min = Math.min(min, rightInfo.min); -// max = Math.max(max, rightInfo.max); -// maxSubBSTSize = Math.max(maxSubBSTSize, rightInfo.maxSubBSTSize); -// } -// boolean isBST = false; -// if ((leftInfo == null ? true : (leftInfo.isAllBST && leftInfo.max < head.value)) -// && (rightInfo == null ? true : (rightInfo.isAllBST && rightInfo.min > head.value))) { -// isBST = true; -// maxSubBSTSize = (leftInfo == null ? 0 : leftInfo.maxSubBSTSize) -// + (rightInfo == null ? 0 : rightInfo.maxSubBSTSize) + 1; -// } -// return new Info(isBST, maxSubBSTSize, min, max); -// } - - // 任何子树,都返回4个信息 - public static class Info { - // 整体是否是二叉搜索树 - public boolean isAllBST; - // 最大的满足二叉搜索树树条件的size - public int maxSubBSTSize; - // 整棵树的最小值 - public int min; - // 整棵树的最大值 - public int max; - - public Info(boolean is, int size, int mi, int ma) { - isAllBST = is; - maxSubBSTSize = size; - min = mi; - max = ma; - } - } - - // 以X为头 - public static Info process(Node X) { - // base case - if(X == null) { - return null; - } - // 默认左树可以给我info信息 - Info leftInfo = process(X.left); - // 默认右树可以给我info信息 - Info rightInfo = process(X.right); - - // 通过左右树给我的信息,加工我自己的info - - int min = X.value; - int max = X.value; - - // 左树不为空,加工min和max - if(leftInfo != null) { - min = Math.min(min, leftInfo.min); - max = Math.max(max, leftInfo.max); - } - // 右树不为空,加工min和max - if(rightInfo != null) { - min = Math.min(min, rightInfo.min); - max = Math.max(max, rightInfo.max); - } - - // 可能性1与X无关的情况 - int maxSubBSTSize = 0; - if(leftInfo != null) { - maxSubBSTSize = leftInfo.maxSubBSTSize; - } - if(rightInfo !=null) { - maxSubBSTSize = Math.max(maxSubBSTSize, rightInfo.maxSubBSTSize); - } - - // 可能性2,与X有关 - boolean isAllBST = false; - - if( - // 左树和人右树整体需要是搜索二叉树 - ( leftInfo == null ? true : leftInfo.isAllBST ) - && - ( rightInfo == null ? true : rightInfo.isAllBST ) - && - // 左树最大值X - (leftInfo == null ? true : leftInfo.max < X.value) - && - (rightInfo == null ? true : rightInfo.min > X.value) - - - ) { - - maxSubBSTSize = - (leftInfo == null ? 0 : leftInfo.maxSubBSTSize) - + - (rightInfo == null ? 0 : rightInfo.maxSubBSTSize) - + - 1; - isAllBST = true; - - - } - - return new Info(isAllBST, maxSubBSTSize, min, max); - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } - - public static void main(String[] args) { - int maxLevel = 4; - int maxValue = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - if (maxSubBSTSize1(head) != maxSubBSTSize2(head)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} -``` - -### 1.1.4 例四:派对最大快乐值 - -排队最大快乐值问题,员工信息定义如下,多叉树结构: - -```Java -class Employee{ - // 这名员工可以带来的快乐值 - public int happy; - // 这名员工有哪些直接的下级 - List subordinates; -} -``` - -每个员工都符合Employee类的描述,整个公司的人员结构可以看作是一颗标准的,没有环的多叉树。树的头结点是公司唯一的老板。除了老板外的每个员工都有唯一的直接上级。叶节点是没有任何下属的基层员工(subordinates为空),除了基层员工股外,每个员工都有一个或多个直接下级。 - - -现在公司要来办party,你可以决定哪些员工来,哪些员工不来,规则: - -1、 如果某个员工来了,那么这个员工的所有直接下级都不能来 - -2、 排队的整体快乐值是所有到场员工的快乐值的累加 - -3、 你的目标是让排队的整体快乐值尽量的大 - -给定一颗多叉树头结点boss,请返回排队的最大快乐值 - -> 思路:根据X来与不来分类 - -> 如果X来,我们能获得X的快乐值X.happy。X的直接子不能来,但我们能拿到X某个子树整棵树的的最大快乐值 - -> 如果X不来,不发请柬,我们能获得X的快乐值为0,X直接子树头结点来或者不来求最大值... - -```Java -package class08; - -import java.util.ArrayList; -import java.util.List; - -public class Code09_MaxHappy { - - // 员工对应的多叉树节点结构 - public static class Employee { - public int happy; - public List nexts; - - public Employee(int h) { - happy = h; - nexts = new ArrayList<>(); - } - - } - - public static int maxHappy1(Employee boss) { - if (boss == null) { - return 0; - } - return process1(boss, false); - } - - public static int process1(Employee cur, boolean up) { - if (up) { - int ans = 0; - for (Employee next : cur.nexts) { - ans += process1(next, false); - } - return ans; - } else { - int p1 = cur.happy; - int p2 = 0; - for (Employee next : cur.nexts) { - p1 += process1(next, true); - p2 += process1(next, false); - } - return Math.max(p1, p2); - } - } - - public static int maxHappy2(Employee boss) { - if (boss == null) { - return 0; - } - Info all = process2(boss); - return Math.max(all.yes, all.no); - } - - // 递归信息 - public static class Info { - // 头结点在来的情况下整棵树的最大值 - public int yes; - // 头结点在不来的情况下整棵树的最大值 - public int no; - - public Info(int y, int n) { - yes = y; - no = n; - } - } - - public static Info process2(Employee x) { - // base case 基层员工 - if (x.nexts.isEmpty()) { - return new Info(x.happy, 0); - } - // 当前X来的初始值 - int yes = x.happy; - // 当前X不来的初始值 - int no = 0; - // 每棵子树调用递归信息 - for (Employee next : x.nexts) { - Info nextInfo = process2(next); - // 根据子树的递归返回的信息,加工自身的info - // 如果X来,子不来 - yes += nextInfo.no; - // 如果X不来,子不确定来不来 - no += Math.max(nextInfo.yes, nextInfo.no); - } - return new Info(yes, no); - } - - // for test - public static Employee genarateBoss(int maxLevel, int maxNexts, int maxHappy) { - if (Math.random() < 0.02) { - return null; - } - Employee boss = new Employee((int) (Math.random() * (maxHappy + 1))); - genarateNexts(boss, 1, maxLevel, maxNexts, maxHappy); - return boss; - } - - // for test - public static void genarateNexts(Employee e, int level, int maxLevel, int maxNexts, int maxHappy) { - if (level > maxLevel) { - return; - } - int nextsSize = (int) (Math.random() * (maxNexts + 1)); - for (int i = 0; i < nextsSize; i++) { - Employee next = new Employee((int) (Math.random() * (maxHappy + 1))); - e.nexts.add(next); - genarateNexts(next, level + 1, maxLevel, maxNexts, maxHappy); - } - } - - public static void main(String[] args) { - int maxLevel = 4; - int maxNexts = 7; - int maxHappy = 100; - int testTimes = 100000; - for (int i = 0; i < testTimes; i++) { - Employee boss = genarateBoss(maxLevel, maxNexts, maxHappy); - if (maxHappy1(boss) != maxHappy2(boss)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} - -``` - -### 1.1.5 例五:判断二叉树是否是满二叉树 - -给定一棵二叉树的头结点head,返回这颗二叉树是不是满二叉树。 - -> 思路:满二叉树一定满足2^L - 1 == N,其中L是这颗二叉树的高度,N是这颗二叉树的节点个数 - -```Java -package class08; - -public class Code02_IsFull { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - public static boolean isFull1(Node head) { - if (head == null) { - return true; - } - int height = h(head); - int nodes = n(head); - return (1 << height) - 1 == nodes; - } - - public static int h(Node head) { - if (head == null) { - return 0; - } - return Math.max(h(head.left), h(head.right)) + 1; - } - - public static int n(Node head) { - if (head == null) { - return 0; - } - return n(head.left) + n(head.right) + 1; - } - - public static boolean isFull2(Node head) { - if (head == null) { - return true; - } - // 如果满足公式是满二叉树 - Info all = process(head); - return (1 << all.height) - 1 == all.nodes; - } - - // 信息 - public static class Info { - public int height; - public int nodes; - - public Info(int h, int n) { - height = h; - nodes = n; - } - } - - // 递归套路 - public static Info process(Node head) { - if (head == null) { - return new Info(0, 0); - } - Info leftInfo = process(head.left); - Info rightInfo = process(head.right); - // 高度 - int height = Math.max(leftInfo.height, rightInfo.height) + 1; - // 节点数 - int nodes = leftInfo.nodes + rightInfo.nodes + 1; - return new Info(height, nodes); - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } - - public static void main(String[] args) { - int maxLevel = 5; - int maxValue = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - if (isFull1(head) != isFull2(head)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} - -``` - -### 1.1.6 例六:二叉搜索树的头结点 - -给定一棵二叉树的头结点head,返回这颗二叉树中最大的二叉搜索子树的头节点 - -> 和前文的返回二叉搜索子树的Size问题类似 - -```Java -package class08; - -import java.util.ArrayList; - -public class Code05_MaxSubBSTHead { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - public static int getBSTSize(Node head) { - if (head == null) { - return 0; - } - ArrayList arr = new ArrayList<>(); - in(head, arr); - for (int i = 1; i < arr.size(); i++) { - if (arr.get(i).value <= arr.get(i - 1).value) { - return 0; - } - } - return arr.size(); - } - - public static void in(Node head, ArrayList arr) { - if (head == null) { - return; - } - in(head.left, arr); - arr.add(head); - in(head.right, arr); - } - - public static Node maxSubBSTHead1(Node head) { - if (head == null) { - return null; - } - if (getBSTSize(head) != 0) { - return head; - } - Node leftAns = maxSubBSTHead1(head.left); - Node rightAns = maxSubBSTHead1(head.right); - return getBSTSize(leftAns) >= getBSTSize(rightAns) ? leftAns : rightAns; - } - - public static Node maxSubBSTHead2(Node head) { - if (head == null) { - return null; - } - return process(head).maxSubBSTHead; - } - - // 每一棵子树Info - public static class Info { - public Node maxSubBSTHead; - public int maxSubBSTSize; - public int min; - public int max; - - public Info(Node h, int size, int mi, int ma) { - maxSubBSTHead = h; - maxSubBSTSize = size; - min = mi; - max = ma; - } - } - - public static Info process(Node X) { - if (X == null) { - return null; - } - Info leftInfo = process(X.left); - Info rightInfo = process(X.right); - int min = X.value; - int max = X.value; - Node maxSubBSTHead = null; - int maxSubBSTSize = 0; - if (leftInfo != null) { - min = Math.min(min, leftInfo.min); - max = Math.max(max, leftInfo.max); - maxSubBSTHead = leftInfo.maxSubBSTHead; - maxSubBSTSize = leftInfo.maxSubBSTSize; - } - if (rightInfo != null) { - min = Math.min(min, rightInfo.min); - max = Math.max(max, rightInfo.max); - if (rightInfo.maxSubBSTSize > maxSubBSTSize) { - maxSubBSTHead = rightInfo.maxSubBSTHead; - maxSubBSTSize = rightInfo.maxSubBSTSize; - } - } - if ((leftInfo == null ? true : (leftInfo.maxSubBSTHead == X.left && leftInfo.max < X.value)) - && (rightInfo == null ? true : (rightInfo.maxSubBSTHead == X.right && rightInfo.min > X.value))) { - maxSubBSTHead = X; - maxSubBSTSize = (leftInfo == null ? 0 : leftInfo.maxSubBSTSize) - + (rightInfo == null ? 0 : rightInfo.maxSubBSTSize) + 1; - } - return new Info(maxSubBSTHead, maxSubBSTSize, min, max); - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } - - public static void main(String[] args) { - int maxLevel = 4; - int maxValue = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - if (maxSubBSTHead1(head) != maxSubBSTHead2(head)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} -``` - -### 1.1.7 例子七:是否是完全二叉树 - -给定一棵二叉树的头结点head,返回这颗二叉树是不是完全二叉树 - -> 完全二叉树概念在堆的章节,有介绍。 - -> 宽度优先遍历解决思路: - -1、如果用树的宽度优先遍历的话,如果某个节点有右孩子,但是没有左孩子,一定不是完全二叉树 - -2、在1条件的基础上,一旦遇到第一个左右孩子不双全的节点,后续所有节点必须为叶子节点 - -> 二叉树递归套路解法思路: - -1、满二叉树(无缺口),一定是完全二叉树。此时左右树需要给X的信息是,是否是满的和高度。如果左右树满,且左右树高度一样,那么是该种情况--满二叉树 - -2、有缺口,1缺口可能停在我的左树上。左树需要给我是否是完全二叉树,右树需要给X是否是满二叉树,且左树高度比右树高度大1 - -3、缺口可能在左右树的分界。左树是满的,右树也是满的,左树高度比右树大1 - -4、左树已经满了,缺口可能在我的右树上。左树是满的,右树是完全二叉树,且左右树高度一样 - -==所以我们的递归时,需要向子树要的信息为:是否是完全二叉树,是否是满二叉树,高度== - -```Java -package class08; - -import java.util.LinkedList; - -public class Code06_IsCBT { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - // 宽度优先遍历解决方法 - public static boolean isCBT1(Node head) { - if (head == null) { - return true; - } - LinkedList queue = new LinkedList<>(); - // 是否遇到过左右两个孩子不双全的节点 - boolean leaf = false; - Node l = null; - Node r = null; - queue.add(head); - while (!queue.isEmpty()) { - head = queue.poll(); - l = head.left; - r = head.right; - if ( - // 如果遇到了不双全的节点之后,又发现当前节点不是叶节点 - (leaf && (l != null || r != null)) || (l == null && r != null) - - ) { - return false; - } - if (l != null) { - queue.add(l); - } - if (r != null) { - queue.add(r); - } - if (l == null || r == null) { - leaf = true; - } - } - return true; - } - - // 递归的解法 - public static boolean isCBT2(Node head) { - if (head == null) { - return true; - } - return process(head).isCBT; - } - - // 对每一棵子树,是否是满二叉树、是否是完全二叉树、高度 - public static class Info { - public boolean isFull; - public boolean isCBT; - public int height; - - public Info(boolean full, boolean cbt, int h) { - isFull = full; - isCBT = cbt; - height = h; - } - } - - // 对于任何节点,我们要返回三个元素组成的Info - public static Info process(Node X) { - // 如果是空树,我们封装Info而不是返回为空 - // 方便下文不需要额外增加判空处理 - if (X == null) { - return new Info(true, true, 0); - } - // 左树info - Info leftInfo = process(X.left); - // 右树info - Info rightInfo = process(X.right); - - // 接下来整合当前X的Info - - // 高度信息=左右树最大高度值+1 - int height = Math.max(leftInfo.height, rightInfo.height) + 1; - - // X是否是满二叉树信息=左右都是满且左右高度一样 - boolean isFull = leftInfo.isFull - && - rightInfo.isFull - && leftInfo.height == rightInfo.height; - - // X是否是完全二叉树 - boolean isCBT = false; - // 满二叉树是完全二叉树 - if (isFull) { - isCBT = true; - // 以x为头整棵树,不满 - } else { - // 左右都是完全二叉树才有讨论的必要 - if (leftInfo.isCBT && rightInfo.isCBT) { - // 第二种情况 - if (leftInfo.isCBT - && rightInfo.isFull - && leftInfo.height == rightInfo.height + 1) { - isCBT = true; - } - // 第三种情况 - if (leftInfo.isFull - && - rightInfo.isFull - && leftInfo.height == rightInfo.height + 1) { - isCBT = true; - } - // 第四种情况 - if (leftInfo.isFull - && rightInfo.isCBT && leftInfo.height == rightInfo.height) { - isCBT = true; - } - - } - } - return new Info(isFull, isCBT, height); - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } - - public static void main(String[] args) { - int maxLevel = 5; - int maxValue = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - if (isCBT1(head) != isCBT2(head)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} - -``` - -### 1.1.8 例子八:最低公共祖先 - -给次那个一颗二叉树的头结点head,和另外两个节点a和b。返回a和b的最低公共祖先 - -二叉树的最低公共祖先概念: 任意两个节点,往父亲看,最开始交汇的节点,就是最低公共祖先 - -> 解法一:用辅助map,Key表示节点,Value表示节点的父亲节点。我们把两个目标节点的父亲以此放到map中,依次遍历 - -> 解法二:使用二叉树的递归套路。 - -1、o1和o2都不在以X为头的树上 - -2、o1和o2有一个在以X为头的树上 - -3、o1和o2都在以X为头的树上 - -3.1、X为头的树,左树右树各有一个 - -3.2、X为头的树,左树含有o1和o2 - -3.3、X为头的树,右树含有o1和o2 - -4、X自身就是o1或者o2,即如果X是o1那么左右树收集到o2即可,如果X是o2,左右树收集到o1即可。 - -```Java -package class08; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; - -public class Code07_lowestAncestor { - - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - // 解法1,借助辅助Map和Set - public static Node lowestAncestor1(Node head, Node o1, Node o2) { - if (head == null) { - return null; - } - // key的父节点是value - HashMap parentMap = new HashMap<>(); - parentMap.put(head, null); - // 递归填充map - fillParentMap(head, parentMap); - // 辅助set - HashSet o1Set = new HashSet<>(); - Node cur = o1; - o1Set.add(cur); - // o1Set存入的是沿途所有的父节点 - while (parentMap.get(cur) != null) { - cur = parentMap.get(cur); - o1Set.add(cur); - } - cur = o2; - // o2的某个父节点在o1Set中,就是我们要找的节点 - while (!o1Set.contains(cur)) { - cur = parentMap.get(cur); - } - return cur; - } - - public static void fillParentMap(Node head, HashMap parentMap) { - if (head.left != null) { - parentMap.put(head.left, head); - fillParentMap(head.left, parentMap); - } - if (head.right != null) { - parentMap.put(head.right, head); - fillParentMap(head.right, parentMap); - } - } - - // 解法1,二叉树递归套路解法 - public static Node lowestAncestor2(Node head, Node o1, Node o2) { - return process(head, o1, o2).ans; - } - - // 任何子树需要的信息结构 - public static class Info { - // o1和o2的最初交汇点,如果不是在当前这颗X节点的树上,返回空 - public Node ans; - // 在当前子树上,是否发现过o1和o2 - public boolean findO1; - public boolean findO2; - - public Info(Node a, boolean f1, boolean f2) { - ans = a; - findO1 = f1; - findO2 = f2; - } - } - - public static Info process(Node X, Node o1, Node o2) { - // o1和o2不为空,那么空树上的Info如下 - if (X == null) { - return new Info(null, false, false); - } - // 左树返回的Info - Info leftInfo = process(X.left, o1, o2); - // 右树返回的Info - Info rightInfo = process(X.right, o1, o2); - - // 构建X自身需要返回的Info - // X为头的树上是否发现了o1 - boolean findO1 = X == o1 || leftInfo.findO1 || rightInfo.findO1; - // X为头的树上是否发现了o2 - boolean findO2 = X == o2 || leftInfo.findO2 || rightInfo.findO2; - // O1和O2最初的交汇点在哪? - - // 1) 在左树上已经提前交汇了,最初交汇点保留左树的 - Node ans = null; - if (leftInfo.ans != null) { - ans = leftInfo.ans; - } - // 2) 在右树上已经提前交汇了,最初交汇点保留右树的 - if (rightInfo.ans != null) { - ans = rightInfo.ans; - } - // 3) 没有在左树或者右树上提前交汇 - if (ans == null) { - // 但是找到了o1和o2,那么交汇点就是X自身 - if (findO1 && findO2) { - ans = X; - } - } - return new Info(ans, findO1, findO2); - } - - // for test - public static Node generateRandomBST(int maxLevel, int maxValue) { - return generate(1, maxLevel, maxValue); - } - - // 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; - } - - // for test - public static Node pickRandomOne(Node head) { - if (head == null) { - return null; - } - ArrayList arr = new ArrayList<>(); - fillPrelist(head, arr); - int randomIndex = (int) (Math.random() * arr.size()); - return arr.get(randomIndex); - } - - // for test - public static void fillPrelist(Node head, ArrayList arr) { - if (head == null) { - return; - } - arr.add(head); - fillPrelist(head.left, arr); - fillPrelist(head.right, arr); - } - - public static void main(String[] args) { - int maxLevel = 4; - int maxValue = 100; - int testTimes = 1000000; - for (int i = 0; i < testTimes; i++) { - Node head = generateRandomBST(maxLevel, maxValue); - Node o1 = pickRandomOne(head); - Node o2 = pickRandomOne(head); - if (lowestAncestor1(head, o1, o2) != lowestAncestor2(head, o1, o2)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } -} -``` - -==二叉树的递归套路,最终转化为基于X只找可能性即可。即树形DP问题== \ No newline at end of file diff --git "a/08-\344\272\214\345\217\211\346\240\221\351\200\222\345\275\222\350\247\243\351\242\230\346\200\235\350\267\257.md" "b/08-\344\272\214\345\217\211\346\240\221\351\200\222\345\275\222\350\247\243\351\242\230\346\200\235\350\267\257.md" new file mode 100644 index 0000000..9c0c485 --- /dev/null +++ "b/08-\344\272\214\345\217\211\346\240\221\351\200\222\345\275\222\350\247\243\351\242\230\346\200\235\350\267\257.md" @@ -0,0 +1,716 @@ +[TOC] +# 1 二叉树的递归解法 + +1、 可以解决面试中的绝大部分二叉树(95%以上)的问题,尤其是树形dp问题 + +2、 其本质是利用递归遍历二叉树的便利性,每个节点在递归的过程中可以回到该节点3次 + + +具体步骤为: +1. 假设以X节点为头,假设可以向X左树和右树要任何信息 +2. 在上一步的假设下,讨论以X为头结点的树,得到答案的可能性(最重要),常见分类是与X无关的答案,与X有关的答案 +3. 列出所有可能性后,确定到底需要向左树和右树要什么样的信息 +4. 把左树信息和右树信息求全集,就是任何一颗子树都需要返回的信息S +5. 递归函数都返回S,每颗子树都这么要求 +6. 写代码,在代码中考虑如何把左树信息和右树信息整合出整棵树的信息 + +## 1.1 二叉树的递归解法实操 + +### 1.1.1 例一:判断二叉树平衡与否 + +给定一棵二叉树的头结点head,返回这颗二叉树是不是平衡二叉树 + +> 平衡树概念:在一棵二叉树中,每一个子树,左树的高度和右树的高度差不超过1 + +> 那么如果以X为头的这颗树,要做到平衡,那么X的左树要是平衡的,右树也是平衡的,且X的左树高度和右树高度差不超过1 + +> 所以该题,我们X需要向左右子树要的信息为,1.高度 2. 是否平衡 + +```Go +package main + +import "math" + +type Node struct { + Val int + Left *Node + Right *Node +} + +// BalanceInfo 表示递归过程中需要收集每个节点的信息 +type BalanceInfo struct { + // 当前节点为头的树是不是平衡的 + IsBalanced bool + // 当前节点为头的树的高度是多少 + Height int +} + +// IsBalanced 给定二叉树头节点,判断该二叉树是不是平衡二叉树 +func (head *Node) IsBalanced() bool { + return Process(head).IsBalanced +} + +func Process(node *Node) *BalanceInfo { + if node == nil { + return &BalanceInfo{ + IsBalanced: true, + Height: 0, + } + } + // 左子树信息 + leftInfo := Process(node.Left) + // 右子树信息 + rightInfo := Process(node.Right) + // 高度等于左右最大高度,加上当前头结点的高度1 + curHeight := int(math.Max(float64(leftInfo.Height), float64(rightInfo.Height))) + 1 + curIsBalanced := true + // 左树不平衡或者右树不平衡,或者左右两子树高度差超过1 + // 那么当前节点为头的树,不平衡 + if !leftInfo.IsBalanced || !rightInfo.IsBalanced || math.Abs(float64(leftInfo.Height) -float64(rightInfo.Height)) > 1 { + curIsBalanced = false + } + // 加工出当前节点的信息返回 + return &BalanceInfo{ + IsBalanced: curIsBalanced, + Height: curHeight, + } +} +``` + +### 1.1.2 例二:返回二叉树任意两个节点最大值 + +给定一棵二叉树的头结点head,任何两个节点之间都存在距离,返回整棵二叉树的最大距离 + +> 1、有可能最大距离和当前节点X无关,即最大距离是X左树的最大距离,或者右树的最大距离 + +> 2、最大距离跟X有关,即最大距离通过X。左树离X最远的点,到X右树上离X最远的点。即X左树的高度加上X自身高度1,加上X右树上的高度 + +> 结论:那么根据递归套路,我们每次递归,需要返回X左树的最大距离和高度,同理返回X右树的最大距离和高度。Info包含最大距离和高度 + +```Go +package main + +import "math" + +type Node struct { + Val int + Left *Node + Right *Node +} + +type DistanceInfo struct { + // 当前节点为树根的情况下,该树的最大距离 + MaxDistance int + // 当前节点为树根的情况下,该树的高度 + Height int +} + +// GetMaxDistance 给定二叉树头节点,求该二叉树的最大距离 +func (head *Node) GetMaxDistance() int { + return Process(head).MaxDistance +} + +func Process(node *Node) *DistanceInfo { + // base case + if node == nil { + return &DistanceInfo{ + MaxDistance: 0, + Height: 0, + } + } + + // 左树信息 + leftInfo := Process(node.Left) + // 右树信息 + rightInfo := Process(node.Right) + // 用左右树的信息,加工当前节点自身的info + // 自身的高度是,左右较大的高度加上自身节点高度1 + curHeight := int(math.Max(float64(leftInfo.Height), float64(rightInfo.Height))) + // 自身最大距离,(左右树最大距离)和(左右树高度相加再加1),求最大值 + curMaxDistance := int(math.Max( + math.Max(float64(leftInfo.MaxDistance), float64(rightInfo.MaxDistance)), + float64(leftInfo.Height+rightInfo.Height+1))) + // 自身的info返回 + return &DistanceInfo{ + MaxDistance: curMaxDistance, + Height: curHeight, + } +} + +``` + +### 1.1.3 例三:返回二叉树中的最大二叉搜索树Size + +给定一颗二叉树的头结点head,返回这颗二叉树中最大的二叉搜索树的Size + +> 搜索二叉树概念:整颗树上没有重复值,左树的值都比我小,右树的值都比我大。每颗子树都如此。 + +> 1、与当前节点X无关,即最终找到的搜索二叉树,不以X为头 + +> 2、与X有关,那么X的左树整体是搜索二叉树,右树同理,且左树的最大值小于X,右树的最小值大于X + +```Go +package main + +import "math" + +type Node struct { + Val int + Left *Node + Right *Node +} + +// Info 任何子树递归过程中,都返回4个信息 +type Info struct { + // 以当前节点为头节点的树,整体是否是二叉搜索树 + IsAllBST bool + // 最大的满足二叉搜索树树条件的size + MaxSubBSTSize int + // 整棵树的最小值 + Min int + // 整棵树的最大值 + Max int +} + +// MaxSubBSTSize 给定二叉树头节点,返回该二叉树的最大二叉搜索子树的大小 +func (head *Node) MaxSubBSTSize() int { + if head == nil { + return 0 + } + return process(head).MaxSubBSTSize +} + +func process(node *Node) *Info { + if node == nil { + return nil + } + + // 左子树返回的Info信息 + leftInfo := process(node.Left) + // 右子树返回的Info信息 + rightInfo := process(node.Right) + + // 加工我自身的info + min,max := node.Val, node.Val + // 左树不为空,加工min和max + if leftInfo != nil { + min = int(math.Min(float64(min), float64(leftInfo.Min))) + max = int(math.Max(float64(max), float64(leftInfo.Max))) + } + + // 右树不为空,加工min和max + if rightInfo != nil { + min = int(math.Min(float64(min), float64(rightInfo.Min))) + max = int(math.Max(float64(max), float64(rightInfo.Max))) + } + + // case1: 与node无关的情况。当前二叉树存在的最大搜索二叉树的最大大小,是左右树存在的最大二叉搜索树的较大的 + maxSubBSTSize := 0 + if leftInfo != nil { + maxSubBSTSize = leftInfo.MaxSubBSTSize + } + if rightInfo != nil { + maxSubBSTSize = int(math.Max(float64(maxSubBSTSize), float64(rightInfo.MaxSubBSTSize))) + } + // 如果当前节点为头的二叉树不是二叉搜索树,则当前Info信息中isAllBST为false + isAllBST := false + + // case2:与node有关的情况 + // 左树整个是二叉搜索树么 + leftIsAllBST := false + // 右树整个是二叉搜索树么 + rightIsAllBST := false + // 左树最大值小于node的值是否 + leftMaxVLessNodeV := false + // 右树的最小值,大于node的值是否 + rightMinMoreNodeV := false + if leftInfo == nil { + leftIsAllBST = true + leftMaxVLessNodeV = true + } else { + leftIsAllBST = leftInfo.IsAllBST + leftMaxVLessNodeV = leftInfo.Max < node.Val + } + + if rightInfo == nil { + rightIsAllBST = true + rightMinMoreNodeV = true + } else { + rightIsAllBST = rightInfo.IsAllBST + rightMinMoreNodeV = rightInfo.Min > node.Val + } + + // 如果左树是二叉搜索树,右树也是二叉搜索树,当前节点为树根的左树最大值都比当前值小,当前节点为树根的右树最小值都比当前值大 + // 证明以当前节点node为树根的树,也是一个二叉搜索树。满足case2 + if leftIsAllBST && rightIsAllBST && leftMaxVLessNodeV && rightMinMoreNodeV { + leftSize := 0 + rightSize := 0 + if leftInfo != nil { + leftSize = leftInfo.MaxSubBSTSize + } + + if rightInfo != nil { + rightSize = rightInfo.MaxSubBSTSize + } + + // 当前节点为树根的二叉搜索树的节点大小是左树存在的最大二叉搜索树的大小,加上右树存在的最大的二叉搜索树的大小,加上当前node节点1 + maxSubBSTSize = leftSize + rightSize + 1 + // 当前节点整个是二叉搜索树 + isAllBST = true + } + + return &Info{ + IsAllBST: isAllBST, + MaxSubBSTSize: maxSubBSTSize, + Min: min, + Max: max, + } + +} +``` + +### 1.1.4 例四:判断二叉树是否是满二叉树 + +给定一棵二叉树的头结点head,返回这颗二叉树是不是满二叉树。 + +> 思路:满二叉树一定满足2^L - 1 == N,其中L是这颗二叉树的高度,N是这颗二叉树的节点个数 + +```Go +package main + +import "math" + +type Node struct { + Val int + Left *Node + Right *Node +} + +// Info 包含高度信息,和节点个数信息 +type Info struct { + Height int + Nodes int +} + +func (head *Node) IsFull() bool { + if head == nil { + return true + } + all := process(head) + // 当前二叉树的高度乘以2 是否等于当前二叉树的节点个数,从而可以判断当前二叉树是否是满二叉树 + return all.Height*2-1 == all.Nodes +} + +func process(node *Node) *Info { + if node == nil { + return &Info{ + Height: 0, + Nodes: 0, + } + } + + leftInfo := process(node.Left) + rightInfo := process(node.Right) + // 当前高度 + height := int(math.Max(float64(leftInfo.Height), float64(rightInfo.Height))) + 1 + // 当前节点为树根的二叉树所有节点数 + nodes := leftInfo.Nodes + rightInfo.Nodes + 1 + return &Info{ + Height: height, + Nodes: nodes, + } +} +``` + +### 1.1.5 例五:二叉搜索树的头结点 + +给定一棵二叉树的头结点head,返回这颗二叉树中最大的二叉搜索子树的头节点 + +和1.1.3问题的返回二叉搜索子树的Size问题类似。 +- 可以在原基础上改动实现 +- 也可以去掉isAllBST的信息,通过判断左树最大二搜索树的head,是不是等于当前节点的左孩子,判断右树最大二搜索树的head,是不是等于当前节点的右孩子,从而判断当前节点为头的树,整个是二叉搜索树,从而构建当前节点递归过程中的Info + +```Go +package main + +import "math" + +type Node struct { + Val int + Left *Node + Right *Node +} + +// Info 任何子树递归过程中,都返回4个信息 +type Info struct { + // 当前节点为头的二叉树中,最大搜索二叉树的头结点 + MaxSubBSTHead *Node + // 以当前节点为头节点的树,整体是否是二叉搜索树 + IsAllBST bool + // 最大的满足二叉搜索树树条件的size + MaxSubBSTSize int + // 整棵树的最小值 + Min int + // 整棵树的最大值 + Max int +} + +func (head *Node) MaxSubBSTHead() *Node { + if head == nil { + return nil + } + return process(head).MaxSubBSTHead +} + +func process(node *Node) *Info { + if node == nil { + return nil + } + + // 左子树返回的Info信息 + leftInfo := process(node.Left) + // 右子树返回的Info信息 + rightInfo := process(node.Right) + + min, max := node.Val, node.Val + var maxSubBSTHead *Node + maxSubBSTSize := 0 + + // 如果当前节点为头的二叉树不是二叉搜索树,则当前Info信息中isAllBST为false + isAllBST := false + + // case1:当前节点为头的二叉树中存在的最大搜索二叉树与当前节点node无关的情况 + // 左树不为空,加工min, max, maxSubBSTHead, maxSubBSTSize + if leftInfo != nil { + min = int(math.Min(float64(min), float64(leftInfo.Min))) + max = int(math.Max(float64(max), float64(leftInfo.Max))) + maxSubBSTHead = leftInfo.MaxSubBSTHead + maxSubBSTSize = leftInfo.MaxSubBSTSize + } + + // 右树不为空,加工min, max, maxSubBSTHead, maxSubBSTSize + if rightInfo != nil { + min = int(math.Min(float64(min), float64(rightInfo.Min))) + max = int(math.Max(float64(max), float64(rightInfo.Max))) + if rightInfo.MaxSubBSTSize > maxSubBSTSize { + maxSubBSTHead = rightInfo.MaxSubBSTHead + maxSubBSTSize = rightInfo.MaxSubBSTSize + } + } + + // case2: 当前节点为头的二叉树中存在的最大搜索二叉树与当前节点node有关的情况 + // 左树整个是二叉搜索树么 + leftIsAllBST := false + // 右树整个是二叉搜索树么 + rightIsAllBST := false + // 左树最大值小于node的值是否 + leftMaxVLessNodeV := false + // 右树的最小值,大于node的值是否 + rightMinMoreNodeV := false + if leftInfo == nil { + leftIsAllBST = true + leftMaxVLessNodeV = true + } else { + leftIsAllBST = leftInfo.IsAllBST + leftMaxVLessNodeV = leftInfo.Max < node.Val + } + + if rightInfo == nil { + rightIsAllBST = true + rightMinMoreNodeV = true + } else { + rightIsAllBST = rightInfo.IsAllBST + rightMinMoreNodeV = rightInfo.Min > node.Val + } + + // 如果左树是二叉搜索树,右树也是二叉搜索树,当前节点为树根的左树最大值都比当前值小,当前节点为树根的右树最小值都比当前值大 + // 证明以当前节点node为树根的树,也是一个二叉搜索树。满足case2 + if leftIsAllBST && rightIsAllBST && leftMaxVLessNodeV && rightMinMoreNodeV { + leftSize := 0 + rightSize := 0 + if leftInfo != nil { + leftSize = leftInfo.MaxSubBSTSize + } + + if rightInfo != nil { + rightSize = rightInfo.MaxSubBSTSize + } + + maxSubBSTHead = node + maxSubBSTSize = leftSize + rightSize + 1 + // 当前节点整个是二叉搜索树 + isAllBST = true + } + + return &Info{ + MaxSubBSTHead: maxSubBSTHead, + IsAllBST: isAllBST, + MaxSubBSTSize: maxSubBSTSize, + Min: min, + Max: max, + } + +} +``` + +### 1.1.6 例子六:是否是完全二叉树 + +给定一棵二叉树的头结点head,返回这颗二叉树是不是完全二叉树 + +> 完全二叉树概念在堆的章节,有介绍。 + +> 宽度优先遍历解决思路: + +1、如果用树的宽度优先遍历的话,如果某个节点有右孩子,但是没有左孩子,一定不是完全二叉树 + +2、在条件1的基础上,一旦遇到第一个左右孩子不双全的节点,后续所有节点必须为叶子节点 + +> 二叉树递归解法思路: + +1、满二叉树(无缺口),一定是完全二叉树。此时左右树需要给X的信息是,是否是满的和高度。如果左右树满,且左右树高度一样,那么是该种情况--满二叉树 + +2、有缺口,1缺口可能停在当前节点左树上。左树需要给当前节点是否是完全二叉树的信息,右树需要给X是否是满二叉树的信息,且左树高度比右树高度大1 + +3、缺口可能在左右树的分界。左树是满的,右树也是满的,左树高度比右树大1 + +4、左树已经满了,缺口可能在我的右树上。左树是满的,右树是完全二叉树,且左右树高度一样 + +> 所以我们的递归时,需要向子树要的信息为:是否是完全二叉树,是否是满二叉树,高度 + +```Go +package main + +import "math" + +type Node struct { + Val int + Left *Node + Right *Node +} + +// IsCBTWithProcess 递归二叉树判断一颗二叉树是否是完全二叉树 +func (head *Node) IsCBTWithProcess() bool { + if head == nil { + return true + } + return process(head).IsCBT +} + +type Info struct { + IsFull bool + IsCBT bool + Height int +} + +func process(node *Node) *Info { + // 如果是空树,我们封装Info而不是返回为空 + // 方便下文不需要额外增加判空处理 + if node == nil { + return &Info{ + IsFull: true, + IsCBT: true, + Height: 0, + } + } + + leftInfo := process(node.Left) + rightInfo := process(node.Right) + + // 整合当前节点的Info + // 高度信息=左右树最大高度值+1 + height := int(math.Max(float64(leftInfo.Height), float64(rightInfo.Height))) + // node是否是满二叉树信息=左右都是满且左右高度一样 + isFull := leftInfo.IsFull && rightInfo.IsFull && leftInfo.Height == rightInfo.Height + isBST := false + if isFull { // 满二叉树是完全二叉树 + isBST = true + } else { // 以node为头整棵树,不满 + // 左右都是完全二叉树才有讨论的必要 + if leftInfo.IsCBT && rightInfo.IsCBT { + // 第二种情况。左树是完全二叉树,右树是满二叉树,左树高度比右树高度大1 + if leftInfo.IsCBT && rightInfo.IsFull && leftInfo.Height == rightInfo.Height + 1 { + isBST = true + } + // 第三种情况。左树满,右树满,且左树高度比右树高度大1 + if leftInfo.IsFull && rightInfo.IsFull && leftInfo.Height == rightInfo.Height + 1 { + isBST = true + } + // 第四种情况。左树满,右树是完全二叉树,且左右树高度相同 + if leftInfo.IsFull && rightInfo.IsCBT && leftInfo.Height == rightInfo.Height { + isBST = true + } + } + } + return &Info{ + IsFull: isFull, + IsCBT: isBST, + Height: height, + } +} + +// IsCBTWithWidth 宽度优先遍历判断一颗二叉树是否是完全二叉树 +func (head *Node)IsCBTWithWidth() bool { + if head == nil { + return true + } + + hd := head + + var queue = make([]*Node, 0) + // 是否遇到过左右两个孩子不双全的节点 + leaf := false + var l *Node = nil + var r *Node = nil + queue = append(queue, hd) + for len(queue) != 0 { + hd = queue[0] + queue = queue[1:] + l = hd.Left + r = hd.Right + // 如果遇到了不双全的节点之后,又发现当前节点不是叶节点 + if leaf && (l != nil || r != nil) || (l == nil && r == nil) { + return false + } + if l != nil { + queue = append(queue, l) + } + if r != nil { + queue = append(queue, r) + } + if l == nil || r == nil { + leaf = true + } + } + return true +} +``` + +### 1.1.7 例子七:最低公共祖先 + +给次那个一颗二叉树的头结点head,和另外两个节点a和b。返回a和b的最低公共祖先 + +二叉树的最低公共祖先概念: 任意两个节点,往父亲看,最开始交汇的节点,就是最低公共祖先 + +> 解法一:用辅助map,Key表示节点,Value表示节点的父亲节点。我们把两个目标节点的父亲以此放到map中,依次遍历 + +> 解法二:使用二叉树的递归套路。 + +1、o1和o2都不在以X为头的树上 + +2、o1和o2有一个在以X为头的树上 + +3、o1和o2都在以X为头的树上 + +3.1、X为头的树,左树右树各有一个 + +3.2、X为头的树,左树含有o1和o2 + +3.3、X为头的树,右树含有o1和o2 + +4、X自身就是o1或者o2,即如果X是o1那么左右树收集到o2即可,如果X是o2,左右树收集到o1即可。 + +```Go +package main + +type Node struct { + Val int + Left *Node + Right *Node +} + +// LowestAncestorByMap 给定两个树节点,求这两个节点的最低公共祖先 +func (head *Node) LowestAncestorByMap(o1 *Node, o2 *Node) *Node { + if head == nil { + return nil + } + // key的父节点是value + parentMap := make(map[*Node]*Node, 0) + parentMap[head] = nil + // 递归填充map + fillParentMap(head, parentMap) + // 辅助set + nodeSet := make(map[*Node]string, 0) + cur := o1 + nodeSet[cur] = "" + // nodeSet存入的是沿途所有的父节点 + for parent, ok := parentMap[cur]; ok; { + nodeSet[parent] = "" + } + + cur = o2 + // o2的某个父节点在parentSet中,就是我们要找的节点 + for _, ok := parentMap[cur]; !ok; { + // 继续向上 + cur = parentMap[cur] + } + return cur +} + +func fillParentMap(head *Node, parentMap map[*Node]*Node) { + if head.Left != nil { + parentMap[head.Left] = head + fillParentMap(head.Left, parentMap) + } + if head.Right != nil { + parentMap[head.Right] = head + fillParentMap(head.Right, parentMap) + } +} + +type Info struct { + // o1和o2的最初交汇点,如果不是在当前这颗X节点的树上,返回空 + Ans *Node + // 在当前子树上,是否发现过o1和o2 + FindO1 bool + FindO2 bool +} + +// LowestAncestorByProcess 递归二叉树判断任意两个节点的最低公共祖先 +func (head *Node) LowestAncestorByProcess(o1, o2 *Node) *Node { + return Process(head, o1, o2).Ans +} + +func Process(node, o1, o2 *Node) *Info { + // o1和o2不为空,那么空树上的Info如下 + if node == nil { + return &Info{ + Ans: nil, + FindO1: false, + FindO2: false, + } + } + leftInfo := Process(node.Left, o1, o2) + rightInfo := Process(node.Right, o1, o2) + + // 构建node自身需要返回的Info + // node为头的树上是否发现了o1 + findO1 := node == o1 || leftInfo.FindO1 || rightInfo.FindO1 + // node为头的树上是否发现了o2 + findO2 := node == o2 || leftInfo.FindO2 || rightInfo.FindO2 + // O1和O2最初的交汇点在哪? + + // 1) 在左树上已经提前交汇了,最初交汇点保留左树的 + var ans *Node = nil + if leftInfo.Ans != nil { + ans = leftInfo.Ans + } + // 2) 在右树上已经提前交汇了,最初交汇点保留右树的 + if rightInfo.Ans != nil { + ans = rightInfo.Ans + } + // 3) 没有在左树或者右树上提前交汇 + if ans == nil { + // 但是找到了o1和o2,那么交汇点就是X自身 + if findO1 && findO2 { + ans = node + } + } + return &Info{ + Ans: ans, + FindO1: findO1, + FindO2: findO2, + } +} +``` + +> 二叉树的递归套路,最终转化为基于X只找可能性即可。即树形DP问题 \ No newline at end of file diff --git "a/09-\350\264\252\345\277\203\347\256\227\346\263\225\346\267\261\345\272\246\350\247\243\346\236\220.md" "b/09-\350\264\252\345\277\203\347\256\227\346\263\225\346\267\261\345\272\246\350\247\243\346\236\220.md" deleted file mode 100644 index 29c404f..0000000 --- "a/09-\350\264\252\345\277\203\347\256\227\346\263\225\346\267\261\345\272\246\350\247\243\346\236\220.md" +++ /dev/null @@ -1,661 +0,0 @@ -[TOC] - -# 1 贪心算法 - -## 1.1 基本概念 - -1、最自然智慧的算法 - -2、用一种局部最功利的标准,总是能做出在当前看来是最好的选择 - -3、难点在于证明局部最优解最功利的标准可以得到全局最优解 - -4、对于贪心算法的学习主要是以增加阅历和经验为主 - -### 1.2.1 贪心算法解释 - -正例:通过一个例子来解释,假设一个数组中N个正数,第一个挑选出来的数乘以1,第二个挑选出来的数乘以2,同理,第N次挑选出来的数乘以N,总的加起来是我们的分数。怎么挑选数字使我们达到最大分数? - -> 数组按从小到大的顺序排序,我们按顺序依次挑选,最终结果就是最大的。本质思想是因子随着挑选次数的增加会增大,我们尽量让大数去结合大的因子。 - -==贪心算法有时是无效的,后面会贪心算法无效的例子== - -### 1.2.2 贪心算法的证明问题 - -如何证明贪心算法的有效性? - -> 一般来说,贪心算法不推荐证明,很多时候证明是非常复杂的。通过下面例子来说明贪心算法证明的复杂性,从头到尾讲一道利用贪心算法求解的题目。 - -例子:给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果。 - -> 字典序概念:直观理解,两个单词放到字典中,从头开始查找这个单词,哪个先被查找到,哪个字典序小。 - -> 字典序严格定义,我们把字符串当成k进制的数,a-z当成26进制的正数,字符长度一样,abk>abc,那么我们说abk的字典序更大。字符长度不一样ac和b,那么我们要把短的用0补齐,0小于a的accil,那么aca即可比较出来大小。 - -Java中字符串的ComparTo方法,就是比较字典序。 - -本题思路1:按照单个元素字典序贪心,例如在[ac,bk,sc,ket]字符串数组中,我们拼接出来最终的字符串字典序最小,那么我们依次挑选字典序最小的进行拼接的贪心策略得到acbkketsc。 - -==但是这样的贪心不一定是正确的,例如[ba,b]按照上述思路的贪心结果是bba,但是bab明显是最小的结果== - -本题思路2:两个元素x和y,x拼接y小于等于x拼接y,那么x放前,否则y放前面。例如x=b,y=ba。bba大于bab的字典与,那么ba放前面 - - -证明: - -我们把拼接当成k进制数的数学运算,把a-z的数当成26进制的数,'ks'拼接'ts'实质是ks * 26^2 + te。 - -目标先证明我们比较的传递性:证明a拼接b小于b拼接a,b拼接c小于等于c拼接b,推出a拼接c小于等于c拼接a。 - -a拼接b等于a乘以k的b长度次方 + b。我们把k的x长度次方这个操作当成m(x)函数。所以: - -```math -a * m(b) + b <= b * m(a) + a - -b * m(c) + c <= c * m(b) + b - -=> - -a * m(b) * c <= b * m(a) * c + ac - bc - -b * m(c) * a + ca - ba <= c * m(b) * a - -=> - -b * m(c) * a + ca - ba <= b * m(a) * c + ac - bc - -=> - -m(c) * a + c <= m(a) * c + a - -``` - -至此,我们证明出我们的排序具有传递性质。 - -根据我们排序策略得到的一组序列,证明我们任意交换两个字符的位置,都会得到更大的字典序。 - - -例如按照思路二得到的amnb序列,我们交换a和b。我们先把a和m交换,由于按照思路二得到的序列,满足a.m <= m.a 那么所以manb > amnb,同理得到amnb < bmna。 - -再证明任意三个交换都会变为更大的字典序,那么最终数学归纳法,得到思路二的正确性 - -==所以贪心算法的证明实质是比较复杂的,我们大可不必每次去证明贪心的正确性== - -```Java -package class09; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashSet; - -public class Code01_LowestLexicography { - - // 暴力法穷举,排列组合 - public static String lowestString1(String[] strs) { - if (strs == null || strs.length == 0) { - return ""; - } - ArrayList all = new ArrayList<>(); - HashSet use = new HashSet<>(); - process(strs, use, "", all); - String lowest = all.get(0); - for (int i = 1; i < all.size(); i++) { - if (all.get(i).compareTo(lowest) < 0) { - lowest = all.get(i); - } - } - return lowest; - } - - // strs里放着所有的字符串 - // 已经使用过的字符串的下标,在use里登记了,不要再使用了 - // 之前使用过的字符串,拼接成了-> path - // 用all收集所有可能的拼接结果 - public static void process(String[] strs, HashSet use, String path, ArrayList all) { - // 所有字符串都是用过了 - if (use.size() == strs.length) { - all.add(path); - } else { - for (int i = 0; i < strs.length; i++) { - if (!use.contains(i)) { - use.add(i); - process(strs, use, path + strs[i], all); - use.remove(i); - } - } - } - } - - public static class MyComparator implements Comparator { - @Override - public int compare(String a, String b) { - return (a + b).compareTo(b + a); - } - } - - // 思路二,贪心解法 - public static String lowestString2(String[] strs) { - if (strs == null || strs.length == 0) { - return ""; - } - Arrays.sort(strs, new MyComparator()); - String res = ""; - for (int i = 0; i < strs.length; i++) { - res += strs[i]; - } - return res; - } - - // for test - public static String generateRandomString(int strLen) { - char[] ans = new char[(int) (Math.random() * strLen) + 1]; - for (int i = 0; i < ans.length; i++) { - int value = (int) (Math.random() * 5); - ans[i] = (Math.random() <= 0.5) ? (char) (65 + value) : (char) (97 + value); - } - return String.valueOf(ans); - } - - // for test - public static String[] generateRandomStringArray(int arrLen, int strLen) { - String[] ans = new String[(int) (Math.random() * arrLen) + 1]; - for (int i = 0; i < ans.length; i++) { - ans[i] = generateRandomString(strLen); - } - return ans; - } - - // for test - public static String[] copyStringArray(String[] arr) { - String[] ans = new String[arr.length]; - for (int i = 0; i < ans.length; i++) { - ans[i] = String.valueOf(arr[i]); - } - return ans; - } - - public static void main(String[] args) { - int arrLen = 6; - int strLen = 5; - int testTimes = 100000; - String[] arr = generateRandomStringArray(arrLen, strLen); - System.out.println("先打印一个生成的字符串"); - for (String str : arr) { - System.out.print(str + ","); - } - System.out.println(); - System.out.println("test begin"); - for (int i = 0; i < testTimes; i++) { - String[] arr1 = generateRandomStringArray(arrLen, strLen); - String[] arr2 = copyStringArray(arr1); - if (!lowestString1(arr1).equals(lowestString2(arr2))) { - for (String str : arr1) { - System.out.print(str + ","); - } - System.out.println(); - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} -``` - -> 全排列的时间复杂度为:O(N!) - -> 每一种贪心算法有可能都有属于他自身的特有证明,例如哈夫曼树算法,证明千变万化 - -==贪心策略算法,尽量不要陷入复杂的证明== - -## 1.2 贪心算法求解思路 - -### 1.2.1 标准求解过程 - -1、分析业务 - -2、根据业务逻辑找到不同的贪心策略 - -3、对于能举出反例的策略,直接跳过,不能举出反例的策略要证明有效性,这往往是比较困难的,要求数学能力很高且不具有统一的技巧性 - -### 1.2.2 贪心算法解题套路 - -1、实现一个不依靠贪心策略的解法X,可以用暴力尝试 - -2、脑补出贪心策略A,贪心策略B,贪心策略C...... - -3、用解法X和对数器,用实验的方式得知哪个贪心策略正确 - -4、不要去纠结贪心策略的证明 - -> 贪心类的题目在笔试中,出现的概率高达6到7成,而面试中出现贪心的概率不到2成。因为笔试要的是淘汰率,面试要的是区分度。由于贪心的解决完全取决于贪心策略有没有想对,有很高的淘汰率但是没有很好的区分度 - -## 1.3 贪心算法套路解题实战 - -### 1.3.1 例一:会议日程安排问题 - -一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目宣讲。给你每个项目的开始时间和结束时间,你来安排宣讲的日程,要求会议室进行宣讲的场数最多。 - -返回最多的宣讲场次。 - -> 思路:本题常见的几种贪心策略,一种是按照谁先开始安排谁,第二种按照持续时间短的先安排,第三种按照谁先结束安排谁。 - -> 通过验证,无需证明得出第三种贪心策略是正确的 - -```Java -package class09; - -import java.util.Arrays; -import java.util.Comparator; - -public class Code04_BestArrange { - - public static class Program { - public int start; - public int end; - - public Program(int start, int end) { - this.start = start; - this.end = end; - } - } - - // 暴力穷举法,用来做对数器 - public static int bestArrange1(Program[] programs) { - if (programs == null || programs.length == 0) { - return 0; - } - return process(programs, 0, 0); - } - - // 还剩什么会议都放在programs里 - // done 之前已经安排了多少会议的数量 - // timeLine表示目前来到的时间点是多少 - - // 目前来到timeLine的时间点,已经安排了done多的会议,剩下的会议programs可以自由安排 - // 返回能安排的最多会议数量 - public static int process(Program[] programs, int done, int timeLine) { - // 没有会议可以安排,返回安排了多少会议的数量 - if (programs.length == 0) { - return done; - } - // 还有会议可以选择 - int max = done; - // 当前安排的会议是什么会,每一个都枚举 - for (int i = 0; i < programs.length; i++) { - if (programs[i].start >= timeLine) { - Program[] next = copyButExcept(programs, i); - max = Math.max(max, process(next, done + 1, programs[i].end)); - } - } - return max; - } - - public static Program[] copyButExcept(Program[] programs, int i) { - Program[] ans = new Program[programs.length - 1]; - int index = 0; - for (int k = 0; k < programs.length; k++) { - if (k != i) { - ans[index++] = programs[k]; - } - } - return ans; - } - - // 解法2:贪心算法 - public static int bestArrange2(Program[] programs) { - Arrays.sort(programs, new ProgramComparator()); - // timeline表示来到的时间点 - int timeLine = 0; - // result表示安排了多少个会议 - int result = 0; - // 由于刚才按照结束时间排序,当前是按照谁结束时间早的顺序遍历 - for (int i = 0; i < programs.length; i++) { - if (timeLine <= programs[i].start) { - result++; - timeLine = programs[i].end; - } - } - return result; - } - - // 根据谁的结束时间早排序 - public static class ProgramComparator implements Comparator { - - @Override - public int compare(Program o1, Program o2) { - return o1.end - o2.end; - } - - } - - // for test - public static Program[] generatePrograms(int programSize, int timeMax) { - Program[] ans = new Program[(int) (Math.random() * (programSize + 1))]; - for (int i = 0; i < ans.length; i++) { - int r1 = (int) (Math.random() * (timeMax + 1)); - int r2 = (int) (Math.random() * (timeMax + 1)); - if (r1 == r2) { - ans[i] = new Program(r1, r1 + 1); - } else { - ans[i] = new Program(Math.min(r1, r2), Math.max(r1, r2)); - } - } - return ans; - } - - public static void main(String[] args) { - int programSize = 12; - int timeMax = 20; - int timeTimes = 1000000; - for (int i = 0; i < timeTimes; i++) { - Program[] programs = generatePrograms(programSize, timeMax); - if (bestArrange1(programs) != bestArrange2(programs)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} - -``` - -### 1.3.2 例二:居民楼路灯问题 - -给定一个字符串str,只由‘X’和‘.’两中国字符构成。 - -‘X’表示墙,不能放灯,也不需要点亮,‘.’表示居民点,可以放灯,需要点亮。 - -如果灯放在i位置,可以让i-1,i和i+1三个位置被点亮,返回如果点亮str中所需要点亮的位置,至少需要几盏灯 - -例如: X..X......X..X. 需要至少5盏灯 - -```Java -package class09; - -import java.util.HashSet; - -public class Code02_Light { - - // 纯暴力,用来做对数器。点的位置放灯和不放灯全排列 - public static int minLight1(String road) { - if (road == null || road.length() == 0) { - return 0; - } - return process(road.toCharArray(), 0, new HashSet<>()); - } - - // str[index....]位置,自由选择放灯还是不放灯 - // str[0..index-1]位置呢?已经做完决定了,那些放了灯的位置,存在lights里 - // 要求选出能照亮所有.的方案,并且在这些有效的方案中,返回最少需要几个灯 - public static int process(char[] str, int index, HashSet lights) { - // index来到结束位置的时候,当前方案准备结束 - if (index == str.length) { - // 检查当前方案能否把所有居民楼都照亮 - for (int i = 0; i < str.length; i++) { - // 当前位置是点的话 - if (str[i] != 'X') { - if (!lights.contains(i - 1) - && !lights.contains(i) - && !lights.contains(i + 1)) { - return Integer.MAX_VALUE; - } - } - } - // 经过for循环的检查,任意点的位置都被照亮了,返回当前有效的一种解 - return lights.size(); - } else { // str还没结束 - // i位置不管是 X 或者 . 都可以选择不放灯 - int no = process(str, index + 1, lights); - int yes = Integer.MAX_VALUE; - // 只有在i位置是.的时候,才可以选择放灯 - if (str[index] == '.') { - lights.add(index); - yes = process(str, index + 1, lights); - lights.remove(index); - } - return Math.min(no, yes); - } - } - - // 贪心解法 - public static int minLight2(String road) { - char[] str = road.toCharArray(); - // index从0出发 - int index = 0; - // 当前灯的个数 - int light = 0; - while (index < str.length) { - // 当前i位置是X,直接跳到下一个位置做决定 - if (str[index] == 'X') { - index++; - // i 位置是 . 不管i+1是X还是.当前区域需要放灯 - } else { - light++; - // 接下来没字符了,遍历结束 - if (index + 1 == str.length) { - break; - } else { - // 如果i+1位置是X,在i位置放灯,去i+2位置做决定 - if (str[index + 1] == 'X') { - index = index + 2; - // i位置是. i+1也是. 那么不管i+2是什么,都在i+1位置放灯,到i+3去做决定 - } else { - index = index + 3; - } - } - } - } - return light; - } - - // for test - public static String randomString(int len) { - char[] res = new char[(int) (Math.random() * len) + 1]; - for (int i = 0; i < res.length; i++) { - res[i] = Math.random() < 0.5 ? 'X' : '.'; - } - return String.valueOf(res); - } - - public static void main(String[] args) { - int len = 20; - int testTime = 100000; - for (int i = 0; i < testTime; i++) { - String test = randomString(len); - int ans1 = minLight1(test); - int ans2 = minLight2(test); - if (ans1 != ans2) { - System.out.println("oops!"); - } - } - System.out.println("finish!"); - } -} - -``` - -### 1.3.3 例三:哈夫曼树问题 - -一根金条切成两半,是需要花费和长度值一样的铜板的。 - -比如长度为20的金条,不管怎么切,都需要花费20个铜板。一群人想整分整块金条,怎么分最省铜板? - -例如:给定数组[10,20,30],代表一共三个人,整块金条长度为60,金条要分成10,20,30三个部分。 - -如果先把长度为60的金条分成10和50,花费60;再把长度为50的金条分成20和30,花费50;一共需要花费110个铜板。但是如果先把长度为60的金条分成30和30,花费60;再把30的金条分成10和20,花费30;一共花费90个铜板。 - -输入一个数组,返回分割的最小代价。 - -> 小根堆,大根堆,排序。是贪心策略最常用的手段,coding代码量很少。因为堆天然就具备根据我们自定义的排序规则重新组织数据 - -```Java -package class09; - -import java.util.PriorityQueue; - -public class Code03_LessMoneySplitGold { - - // 暴力解法 - public static int lessMoney1(int[] arr) { - if (arr == null || arr.length == 0) { - return 0; - } - return process(arr, 0); - } - - public static int process(int[] arr, int pre) { - if (arr.length == 1) { - return pre; - } - int ans = Integer.MAX_VALUE; - // 穷举任意两个结合后的后续 - for (int i = 0; i < arr.length; i++) { - for (int j = i + 1; j < arr.length; j++) { - ans = Math.min(ans, process(copyAndMergeTwo(arr, i, j), pre + arr[i] + arr[j])); - } - } - return ans; - } - - public static int[] copyAndMergeTwo(int[] arr, int i, int j) { - int[] ans = new int[arr.length - 1]; - int ansi = 0; - for (int arri = 0; arri < arr.length; arri++) { - if (arri != i && arri != j) { - ans[ansi++] = arr[arri]; - } - } - ans[ansi] = arr[i] + arr[j]; - return ans; - } - - // 贪心解法,建立一个小根堆,把所有数扔进去 - public static int lessMoney2(int[] arr) { - PriorityQueue pQ = new PriorityQueue<>(); - for (int i = 0; i < arr.length; i++) { - pQ.add(arr[i]); - } - int sum = 0; - int cur = 0; - while (pQ.size() > 1) { - // 每一次弹出两个数,合并成一个数 - // 合成的数一定输非叶子节点 - cur = pQ.poll() + pQ.poll(); - // 把合成的数累加到sum中去 - sum += cur; - // 把合成的数加入小根堆中 - pQ.add(cur); - } - return sum; - } - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) (Math.random() * (maxValue + 1)); - } - return arr; - } - - public static void main(String[] args) { - int testTime = 100000; - int maxSize = 6; - int maxValue = 1000; - for (int i = 0; i < testTime; i++) { - int[] arr = generateRandomArray(maxSize, maxValue); - if (lessMoney1(arr) != lessMoney2(arr)) { - System.out.println("Oops!"); - } - } - System.out.println("finish!"); - } - -} -``` - -### 1.3.4 例四:项目花费和利润问题 - -输入:正数数组costs,正数数组profits,正数K,正数M - -costs[i]表示i号项目的花费,profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润) - -K表示你只能串行的最多K个项目,M表示你的初始资金。 - -说明:每做完一个项目,马上获得收益,可以支持你去做下一个项目。不能并行的做项目。 - -输出:你最后获得的最大钱数。 - -```Java -package class09; - -import java.util.Comparator; -import java.util.PriorityQueue; - -public class Code05_IPO { - - public static int findMaximizedCapital(int K, int W, int[] Profits, int[] Capital) { - // 由花费组织的小根堆 - PriorityQueue minCostQ = new PriorityQueue<>(new MinCostComparator()); - // 由利润组织的大根堆 - PriorityQueue maxProfitQ = new PriorityQueue<>(new MaxProfitComparator()); - - // 把所有项目加入到由花费组织的小根堆里 - for (int i = 0; i < Profits.length; i++) { - minCostQ.add(new Program(Profits[i], Capital[i])); - } - // 做k轮项目 - for (int i = 0; i < K; i++) { - // 小根堆不为空,且堆顶的花费被我当前启动资金cover住 - while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) { - // 小根堆的堆顶扔到大根堆中去 - maxProfitQ.add(minCostQ.poll()); - } - // 大根堆没有可以做的项目,直接返回所有钱数 - if (maxProfitQ.isEmpty()) { - return W; - } - // 大根堆不为空,堆顶元素的利润直接加到我们的总钱数上 - // 大根堆弹出堆顶元素 - W += maxProfitQ.poll().p; - } - return W; - } - - // 项目实体类 - public static class Program { - public int p; - public int c; - - public Program(int p, int c) { - this.p = p; - this.c = c; - } - } - - // 根据花费组织的小根堆的比较器 - public static class MinCostComparator implements Comparator { - - @Override - public int compare(Program o1, Program o2) { - return o1.c - o2.c; - } - - } - - // 根据利润组织的大根堆的比较器 - public static class MaxProfitComparator implements Comparator { - - @Override - public int compare(Program o1, Program o2) { - return o2.p - o1.p; - } - - } - -} - -``` diff --git "a/09-\350\264\252\345\277\203\347\256\227\346\263\225\350\247\243\351\242\230\346\200\235\350\267\257.md" "b/09-\350\264\252\345\277\203\347\256\227\346\263\225\350\247\243\351\242\230\346\200\235\350\267\257.md" new file mode 100644 index 0000000..784741f --- /dev/null +++ "b/09-\350\264\252\345\277\203\347\256\227\346\263\225\350\247\243\351\242\230\346\200\235\350\267\257.md" @@ -0,0 +1,457 @@ +[TOC] + +# 1 贪心算法 + +## 1.1 基本概念 + +1、最自然智慧的算法 + +2、用一种局部最功利的标准,总是能做出在当前看来是最好的选择 + +3、难点在于证明局部最优解可以最终得到全局最优解 + +4、对于贪心算法的学习主要是以经验为主,尝试为主 + +### 1.2.1 贪心算法解释 + +正例:通过一个例子来解释,假设一个数组中N个正数,第一个挑选出来的数乘以1,第二个挑选出来的数乘以2,同理,第N次挑选出来的数乘以N,总的加起来是我们的分数。怎么挑选数字使我们达到最大分数? + +> 数组按从小到大的顺序排序,我们按顺序依次挑选,最终结果就是最大的。本质思想是因子随着挑选次数的增加会增大,我们尽量让大数去结合大的因子。 + +> 贪心算法有时是无效的,下文会举贪心算法无效的例子 + +### 1.2.2 贪心算法的证明问题 + +如何证明贪心算法的有效性? + +> 一般来说,贪心算法不推荐证明,很多时候证明是非常复杂的。通过下面例子来说明贪心算法证明的复杂性; + +例子:给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果。 + +> 字典序概念:直观理解,两个单词放到字典中,从头开始查找这个单词,哪个先被查找到,哪个字典序小。 + +> 字典序严格定义,我们把字符串当成k进制的数,a-z当成26进制的正数,字符长度一样,abk>abc,那么我们说abk的字典序更大。字符长度不一样ac和b,那么我们要把短的用0补齐,0小于a的accil,那么aca即可比较出来大小。 + +常见的语言标准库中比较字符串的函数,大都是比较字典序。 + +思路1:按照单个元素字典序贪心,例如在[ac,bk,sc,ket]字符串数组中,我们拼接出来最终的字符串字典序最小,那么我们依次挑选字典序最小的进行拼接的贪心策略得到acbkketsc。 + +> 但是这样的贪心不一定是正确的,例如[ba,b]按照上述思路的贪心结果是bba,但是bab明显是最小的结果 + +思路2:两个元素x和y,x拼接y小于等于y拼接x,那么x放前,否则y放前面。例如x=b,y=ba。bba大于bab的字典序,那么ba放前面 + + +证明: + +我们把拼接当成k进制数的数学运算,把a-z的数当成26进制的数,'ks'拼接'ts'实质是ks * 26^2 + te。 + +目标先证明我们比较的传递性:证明a拼接b小于b拼接a,b拼接c小于等于c拼接b,推导出a拼接c小于等于c拼接a。 + +a拼接b等于a乘以k的b长度次方 + b。我们把k的x长度次方这个操作当成m(x)函数。所以: + +```math +a * m(b) + b <= b * m(a) + a + +b * m(c) + c <= c * m(b) + b + +=> + +a * m(b) * c <= b * m(a) * c + ac - bc + +b * m(c) * a + ca - ba <= c * m(b) * a + +=> + +b * m(c) * a + ca - ba <= b * m(a) * c + ac - bc + +=> + +m(c) * a + c <= m(a) * c + a + +``` + +至此,我们证明出我们的排序具有传递性质。 + +根据我们排序策略得到的一组序列,证明我们任意交换两个字符的位置,都会得到更大的字典序。 + + +例如按照思路二得到的amnb序列,我们交换a和b。我们先把a和m交换,由于按照思路二得到的序列,满足a.m <= m.a 那么所以manb > amnb,同理得到amnb < bmna。 + +再证明任意三个交换都会变为更大的字典序,那么最终数学归纳法,得到思路二的正确性 + +> 所以贪心算法的证明实质是比较复杂的,我们大可不必每次去证明贪心的正确性 + +```Go +package main + +import ( + "sort" + "strings" +) + +// 方法1 暴力法穷举,排列组合。略 + +// LowestStringByGreedy 方法2 贪心法 +func LowestStringByGreedy(strs []string) string { + if len(strs) == 0 { + return "" + } + + Sort(strs, func(a, b string) int { + return strings.Compare(a, b) + }) + + res := "" + for i := 0; i < len(strs); i++ { + res += strs[i] + } + return res +} + +type Comparator func(a, b string) int + +func Sort(values []string, comparator Comparator) { + sort.Sort(sortable{values, comparator}) +} + +type sortable struct { + values []string + comparator Comparator +} + +func (s sortable) Len() int { + return len(s.values) +} + +func (s sortable) Swap(i, j int) { + s.values[i], s.values[j] = s.values[j], s.values[i] +} + +func (s sortable) Less(i, j int) bool { + return s.comparator(s.values[i], s.values[j]) < 0 +} +``` + +> 全排列的时间复杂度为:O(N!) + +> 每一种贪心算法有可能都有属于他自身的特有证明,例如哈夫曼树算法,证明千变万化 + +> 贪心策略算法,尽量不要陷入复杂的证明 + +## 1.2 贪心算法求解思路 + +### 1.2.1 标准求解过程 + +1、分析业务 + +2、根据业务逻辑找到不同的贪心策略 + +3、对于能举出反例的策略,直接跳过,不能举出反例的策略要证明有效性,这往往是比较困难的,要求数学能力很高且不具有统一的技巧性 + +### 1.2.2 贪心算法解题套路 + +1、实现一个不依靠贪心策略的解法X,可以用暴力尝试 + +2、脑补出贪心策略A,贪心策略B,贪心策略C...... + +3、用解法X和对数器,用实验的方式得知哪个贪心策略正确 + +4、不要去纠结贪心策略的证明 + +> 贪心类的题目在笔试中,出现的概率高达6到7成,而面试中出现贪心的概率不到2成。因为笔试要的是淘汰率,面试要的是区分度。由于贪心的解决完全取决于贪心策略有没有想对,有很高的淘汰率但是没有很好的区分度 + +## 1.3 贪心算法套路解题实战 + +### 1.3.1 例一:会议日程安排问题 + +一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目宣讲。给你每个项目的开始时间和结束时间,你来安排宣讲的日程,要求会议室进行宣讲的场数最多。 + +返回最多的宣讲场次。 + +> 思路:本题常见的几种贪心策略,一种是按照谁先开始安排谁,第二种按照持续时间短的先安排,第三种按照谁先结束安排谁。 + +> 通过验证,无需证明得出第三种贪心策略是正确的 + +```Go +package main + +import "sort" + +type Program struct { + start int + end int +} + +type Programs []*Program + +func bestArrange(programs Programs) int { + sort.Sort(programs) + // timeline表示来到的时间点 + timeLine := 0 + // result表示安排了多少个会议 + result := 0 + // 由于刚才按照结束时间排序,当前是按照谁结束时间早的顺序遍历 + for i := 0; i < len(programs); i++ { + if timeLine <= programs[i].start { + result++ + timeLine = programs[i].end + } + } + return result +} + +func (p Programs) Len() int { + return len(p) +} + +// Less 根据谁的结束时间早排序 +func (p Programs) Less(i, j int) bool { + return p[i].end-p[j].end > 0 +} + +func (p Programs) Swap(i, j int) { + p[i], p[j] = p[j], p[i] +} +``` + +### 1.3.2 例二:居民楼路灯问题 + +给定一个字符串str,只由‘X’和‘.’两个字符构成。 + +‘X’表示墙,不能放灯,也不需要点亮,‘.’表示居民点,可以放灯,需要点亮。 + +如果灯放在i位置,可以让i-1,i和i+1三个位置被点亮,返回如果点亮str中所需要点亮的位置,至少需要几盏灯 + +例如: X..X......X..X. 需要至少5盏灯 + +```Go +package main + +// minLight 给定一个由'X'和'.'组成的居民楼路径。要照亮所有居民楼,返回最少需要几盏灯 +func minLight(road string) int { + str := []byte(road) + // index从0出发 + index := 0 + // 当前灯的个数 + light := 0 + for index < len(str) { + // 当前i位置是X,直接跳到下一个位置做决定 + if str[index] == 'X' { + index++ + } else { // i 位置是 . 不管i+1是X还是.当前区域需要放灯 + light++ + // 接下来没字符了,遍历结束 + if index + 1 == len(str) { + break + } else { + // 如果i+1位置是X,在i位置放灯,去i+2位置做决定 + if str[index + 1] == 'X' { + index = index + 2 + } else { // i位置是. i+1也是. 那么不管i+2是什么,都在i+1位置放灯,到i+3去做决定 + index = index + 3 + } + } + } + } + return light +} +``` + +### 1.3.3 例三:哈夫曼树问题 + +一根金条切成两半,是需要花费和长度值一样的铜板的。 + +比如长度为20的金条,不管怎么切,都需要花费20个铜板。一群人想整分整块金条,怎么分最省铜板? + +例如:给定数组[10,20,30],代表一共三个人,整块金条长度为60,金条要分成10,20,30三个部分。 + +如果先把长度为60的金条分成10和50,花费60;再把长度为50的金条分成20和30,花费50;一共需要花费110个铜板。但是如果先把长度为60的金条分成30和30,花费60;再把30的金条分成10和20,花费30;一共花费90个铜板。 + +输入一个数组,返回分割的最小代价。 + +> 小根堆,大根堆,排序。是贪心策略最常用的手段,coding代码量很少。因为堆天然就具备根据我们自定义的排序规则重新组织数据 + +```Go +package main + +import ( + "container/heap" + "fmt" +) + +// CutCost Array 例如[10, 20, 30]表示价值为60的金条,需要切割成10 20 30的条段给三个人分 +type CutCost struct { + Array []int +} + +func (c CutCost)Len() int { + return len(c.Array) +} + +func (c CutCost)Less(i, j int) bool { + return c.Array[i] > c.Array[j] +} + +func (c CutCost)Swap(i, j int) { + c.Array[i], c.Array[j] = c.Array[j], c.Array[i] +} + +func (c *CutCost) Push(h interface{}) { + c.Array = append(c.Array, h.(int)) +} + +func (c *CutCost) Pop() (x interface{}) { + n := len(c.Array) + x = c.Array[n - 1] + c.Array = c.Array[:n-1] + return x +} + +// 切金条,贪心解法,建立一个小根堆,把所有数扔进去 +func lessMoney (c *CutCost) int { + fmt.Println("原始slice: ", c.Array) + + heap.Init(c) + // 通过堆初始化后的arr + fmt.Println("堆初始化后的slice:", c.Array) + + sum := 0 + cur := 0 + for len(c.Array) > 1 { + // 每一次弹出两个数,合并成一个数 + // 合成的数一定输非叶子节点 + cur = c.Pop().(int) + c.Pop().(int) + // 把合成的数累加到sum中去 + sum += cur + // 把合成的数加入小根堆中 + c.Push(cur) + } + return sum +} +``` + +### 1.3.4 例四:项目花费和利润问题 + +输入:正数数组costs,正数数组profits,正数K,正数M + +costs[i]表示i号项目的花费,profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润) + +K表示你只能串行的最多K个项目,M表示你的初始资金。 + +说明:每做完一个项目,马上获得收益,可以支持你去做下一个项目。不能并行的做项目。 + +输出:你最后获得的最大钱数。 + +```Go +package main + +import ( + "container/heap" +) + +// Item 项目 +type Item struct { + C int + P int +} + +// MinCostQ 项目最小花费。由花费组织的小根堆 +type MinCostQ struct { + Items []*Item +} + +func (c MinCostQ) Len() int { + return len(c.Items) +} + +// Less i对应的花费C的值小于j对应的值为true,则从小到大排序,即小根堆 +func (c MinCostQ) Less(i, j int) bool { + return c.Items[i].C < c.Items[j].C +} + +func (c MinCostQ) Swap(i, j int) { + c.Items[i], c.Items[j] = c.Items[j], c.Items[i] +} + +func (c *MinCostQ) Push(h interface{}) { + c.Items = append(c.Items, h.(*Item)) +} + +func (c *MinCostQ) Pop() (x interface{}) { + n := len(c.Items) + x = c.Items[n-1] + c.Items = c.Items[:n-1] + return x +} + +// MaxProfitQ 项目最大利润,由利润组织的大根堆 +type MaxProfitQ struct { + Items []*Item +} + +func (c MaxProfitQ) Len() int { + return len(c.Items) +} + +// Less i对应的利润P的值大于j对应的值为true,则从大到小排序,即大根堆 +func (c MaxProfitQ) Less(i, j int) bool { + return c.Items[i].P > c.Items[j].P +} + +func (c MaxProfitQ) Swap(i, j int) { + c.Items[i], c.Items[j] = c.Items[j], c.Items[i] +} + +func (c *MaxProfitQ) Push(h interface{}) { + c.Items = append(c.Items, h.(*Item)) +} + +func (c *MaxProfitQ) Pop() (x interface{}) { + n := len(c.Items) + x = c.Items[n-1] + c.Items = c.Items[:n-1] + return x +} + +// findMaximizedCapital 找到项目最大利润。由于Profits和Capital一一对应 +// K表示你只能串行的最多K个项目,M表示你的初始资金。 +func findMaximizedCapital(K, W int, Profits, Capital []int) int { + Items := make([]*Item, 0) + for i := 0; i < len(Profits); i++ { + im := &Item{ + C: Capital[i], + P: Profits[i], + } + Items = append(Items, im) + } + minC := &MinCostQ{ + Items: Items, + } + + maxQ := &MaxProfitQ{ + Items: Items, + } + + // 由花费组织的小根堆。初始化 + heap.Init(minC) + // 由利润组织的大根堆。初始化 + heap.Init(maxQ) + + // 做k轮项目 + for i := 0; i < K; i++ { + // 小根堆不为空,且堆顶的花费被我当前启动资金cover住 + for len(minC.Items) != 0 && minC.Items[len(minC.Items) - 1].C <= W { + // 小根堆的堆顶扔到大根堆中去 + maxQ.Push(minC.Pop()) + } + // 大根堆没有可以做的项目,直接返回总钱数 + if len(maxQ.Items) == 0 { + return W + } + // 大根堆不为空,堆顶元素的利润直接加到我们的总钱数上 + // 大根堆弹出堆顶元素 + W += maxQ.Pop().(Item).P + } + return W +} +``` 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 71d733e..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" @@ -23,247 +23,111 @@ void union(V x, V y):把x和y各自所在集合的所有样本合并成一个 > 并查集的优化点主要有两个,一个是合并的时候小的集合挂在大的集合下面,第二个优化是找某节点最上方的代表节点,把沿途节点全部拍平,下次再找该沿途节点,都变为O(1)。两种优化的目的都是为了更少的遍历节点。 -> 由于我们加入了优化,如果N个节点,我们调用findFather越频繁,我们的时间复杂度越低,因为第一次调用我们加入了优化。如果findFather调用接近N次或者远远超过N次,我们并查集的时间复杂度就是O(1)。该复杂度只需要记住结论,证明无须掌握。该证明从1964年一直研究到1989年,整整25年才得出证明!算法导论23章,英文版接近50页的证明。 +> 由于我们加入了优化,如果N个节点,我们调用findFather越频繁,我们的时间复杂度越低,因为第一次调用我们加入了优化。如果findFather调用接近N次或者远远超过N次,我们并查集的时间复杂度就是O(1)。该证明从1964年一直研究到1989年,整整25年,算法导论23章,英文版接近50页。 -```Java -package class10; +```Go +package main -import java.util.HashMap; -import java.util.List; -import java.util.Stack; - -public class Code01_UnionFind { +// Node 并查集结构中的节点类型 +type Node struct { + V int +} - // 并查集结构中的节点类型 - public static class Node { - V value; +type UnionSet struct { + // 记录样本到样本代表点的关系。值到代表该值的Node节点的关系映射 + Nodes map[int]*Node + // 记录某节点到父亲节点的关系。 + // 比如b指向a,c指向a,d指向a,a指向自身 + // map中保存的a->a b->a c->a d->a + Parents map[*Node]*Node + // 只有当前点,他是代表点,会在sizeMap中记录该代表点的连通个数 + SizeMap map[*Node]int +} - public Node(V v) { - value = v; - } +// FindFather 在并查集结构中找一个节点的父亲根节点 +// 从点cur开始,一直往上找,找到不能再往上的代表点,返回 +// 通过把路径上所有节点指向最上方的代表节点,目的是把findFather优化成O(1)的 +func (set *UnionSet) FindFather(cur *Node) *Node { + // 在找father的过程中,沿途所有节点加入当前容器,便于后面扁平化处理 + path := make([]*Node, 0) + // 当前节点的父亲不是指向自己,进行循环 + for cur != set.Parents[cur] { + path = append(path, cur) + // 向上移动 + cur = set.Parents[cur] } - - public static class UnionSet { - // 记录样本到样本代表点的关系 - public HashMap> nodes; - // 记录某节点到父亲节点的关系。 - // 比如b指向a,c指向a,d指向a,a指向自身 - // map中保存的a->a b->a c->a d->a - public HashMap, Node> parents; - // 只有当前点,他是代表点,会在sizeMap中记录该代表点的连通个数 - public HashMap, Integer> sizeMap; - - // 初始化构造一批样本 - public UnionSet(List values) { - // 每个样本的V指向自身的代表节点 - // 每个样本当前都是独立的,parent是自身 - // 每个样本都是代表节点放入sizeMap - for (V cur : values) { - Node node = new Node<>(cur); - nodes.put(cur, node); - parents.put(node, node); - sizeMap.put(node, 1); - } - } - - // 从点cur开始,一直往上找,找到不能再往上的代表点,返回 - // 通过把路径上所有节点指向最上方的代表节点,目的是把findFather优化成O(1)的 - public Node findFather(Node cur) { - // 在找father的过程中,沿途所有节点加入当前容器,便于后面扁平化处理 - Stack> path = new Stack<>(); - // 当前节点的父亲不是指向自己,进行循环 - while (cur != parents.get(cur)) { - path.push(cur); - cur = parents.get(cur); - } - // 循环结束,cur是最上的代表节点 - // 把沿途所有节点拍平,都指向当前最上方的代表节点 - while (!path.isEmpty()) { - parents.put(path.pop(), cur); - } - return cur; - } - - // isSameSet方法 - public boolean isSameSet(V a, V b) { - // 先检查a和b有没有登记 - if (!nodes.containsKey(a) || !nodes.containsKey(b)) { - return false; - } - // 比较a的最上的代表点和b最上的代表点 - return findFather(nodes.get(a)) == findFather(nodes.get(b)); - } - - // union方法 - public void union(V a, V b) { - // 先检查a和b有没有都登记过 - if (!nodes.containsKey(a) || !nodes.containsKey(b)) { - return; - } - - // 找到a的最上面的代表点 - Node aHead = findFather(nodes.get(a)); - // 找到b的最上面的代表点 - Node bHead = findFather(nodes.get(b)); - - // 只有两个最上代表点内存地址不相同,需要union - if (aHead != bHead) { - - // 由于aHead和bHead都是代表点,那么在sizeMap里可以拿到大小 - int aSetSize = sizeMap.get(aHead); - int bSetSize = sizeMap.get(bHead); - - // 哪个小,哪个挂在下面 - Node big = aSetSize >= bSetSize ? aHead : bHead; - Node small = big == aHead ? bHead : aHead; - // 把小集合直接挂到大集合的最上面的代表节点下面 - parents.put(small, big); - // 大集合的代表节点的size要吸收掉小集合的size - sizeMap.put(big, aSetSize + bSetSize); - // 把小的记录删除 - sizeMap.remove(small); - } + // 循环结束,cur此时是最上的代表节点 + // 把沿途所有节点拍平,都指向当前最上方的代表节点 + for len(path) != 0 { + for i := len(path) - 1; i >= 0; i-- { + set.Parents[path[i]] = cur + path = path[:len(path) - 1] // 模拟栈的弹出 } } - + return cur } -``` -==并查集用来处理连通性的问题特别方便== - -### 1.1.2 例题 - -学生实例有三个属性,身份证信息,B站ID,Github的Id。我们认为,任何两个学生实例,只要身份证一样,或者B站ID一样,或者Github的Id一样,我们都算一个人。给定一打拼学生实例,输出有实质有几个人? - -> 思路:把实例的三个属性建立三张映射表,每个实例去对比,某个实例属性在表中能查的到,需要联通该实例到之前保存该实例属性的头结点下 - - -```Java -package class10; - -import java.util.HashMap; -import java.util.List; -import java.util.Stack; -public class Code07_MergeUsers { - - public static class Node { - V value; - - public Node(V v) { - value = v; - } +// IsSameSet 判断两个元素是否在同一个并查集中 +func (set *UnionSet) IsSameSet(a, b int) bool { + // 先检查a和b有没有登记 + if _, ok := set.Nodes[a]; !ok { + return false } - - public static class UnionSet { - public HashMap> nodes; - public HashMap, Node> parents; - public HashMap, Integer> sizeMap; - - public UnionSet(List values) { - for (V cur : values) { - Node node = new Node<>(cur); - nodes.put(cur, node); - parents.put(node, node); - sizeMap.put(node, 1); - } - } - - // 从点cur开始,一直往上找,找到不能再往上的代表点,返回 - public Node findFather(Node cur) { - Stack> path = new Stack<>(); - while (cur != parents.get(cur)) { - path.push(cur); - cur = parents.get(cur); - } - // cur头节点 - while (!path.isEmpty()) { - parents.put(path.pop(), cur); - } - return cur; - } - - public boolean isSameSet(V a, V b) { - if (!nodes.containsKey(a) || !nodes.containsKey(b)) { - return false; - } - return findFather(nodes.get(a)) == findFather(nodes.get(b)); - } - - public void union(V a, V b) { - if (!nodes.containsKey(a) || !nodes.containsKey(b)) { - return; - } - Node aHead = findFather(nodes.get(a)); - Node bHead = findFather(nodes.get(b)); - if (aHead != bHead) { - int aSetSize = sizeMap.get(aHead); - int bSetSize = sizeMap.get(bHead); - Node big = aSetSize >= bSetSize ? aHead : bHead; - Node small = big == aHead ? bHead : aHead; - parents.put(small, big); - sizeMap.put(big, aSetSize + bSetSize); - sizeMap.remove(small); - } - } - - - public int getSetNum() { - return sizeMap.size(); - } - + if _, ok := set.Nodes[b]; !ok { + return false } - public static class User { - public String a; - public String b; - public String c; - - public User(String a, String b, String c) { - this.a = a; - this.b = b; - this.c = c; - } + // 比较a的最上的代表点和b最上的代表点 + return set.FindFather(set.Nodes[a]) == set.FindFather(set.Nodes[b]) +} +// Union 合并两个元素 +func (set *UnionSet) Union(a, b int) { + // 先检查a和b有没有都登记过 + if _, ok := set.Nodes[a]; !ok { + return } - - // (1,10,13) (2,10,37) (400,500,37) - // 如果两个user,a字段一样、或者b字段一样、或者c字段一样,就认为是一个人 - // 请合并users,返回合并之后的用户数量 - public static int mergeUsers(List users) { - UnionSet unionFind = new UnionSet<>(users); - HashMap mapA = new HashMap<>(); - HashMap mapB = new HashMap<>(); - HashMap mapC = new HashMap<>(); - for(User user : users) { - if(mapA.containsKey(user.a)) { - unionFind.union(user, mapA.get(user.a)); - }else { - mapA.put(user.a, user); - } - if(mapB.containsKey(user.b)) { - unionFind.union(user, mapB.get(user.b)); - }else { - mapB.put(user.b, user); - } - if(mapC.containsKey(user.c)) { - unionFind.union(user, mapC.get(user.c)); - }else { - mapC.put(user.c, user); - } - } - // 向并查集询问,合并之后,还有多少个集合? - return unionFind.getSetNum(); + if _, ok := set.Nodes[b]; !ok { + return } + // 找到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 *Node + var small *Node + // 哪个小,哪个挂在下面 + if aSetSize >= bSetSize { + big = aHead + small = bHead + } else { + big = bHead + small = aHead + } + + // 把小集合直接挂到大集合的最上面的代表节点下面 + set.Parents[small] = big + // 大集合的代表节点的size要吸收掉小集合的size + set.SizeMap[big] = aSetSize + bSetSize + // 把被吸收掉的小set删除掉 + delete(set.SizeMap, small) + } } ``` +> 并查集用来处理连通性的问题特别方便 ## 1.2 图相关算法 ### 1.2.1 图的概念 1、由点的集合和边的集合构成 -2、虽然存在有向图和无向图的概念,但实际上都可以用有向图来表达,无向图可以理解为两个联通点互相指向 +2、虽然存在有向图和无向图的概念,但实际上都可以用有向图来表达,无向图可以理解为两个联通点互相指向的有向图 3、边上可能带有权值 @@ -295,8 +159,9 @@ C: A,B D: B,A -如果是带有权重的边,可以封装我们的结构,例如A到C的权重是3,那么我们可以表示为A: C(3),D +A可以直接到达C和D, B可以直接到大C和D, C可以直接到达A和B, D可以直接到达B和A。图的邻接表表示法 +如果是带有权重的边,可以封装我们的结构,例如A到C的权重是3,那么我们可以表示为A: C(3),D #### 1.2.2.2 邻接矩阵表示法 @@ -310,138 +175,52 @@ C 3 0 0 - D 0 0 - 0 ``` -> 图算法并不难,难点在于图有很多种表示方式,表达一张图的篇幅比较大,coding容易出错。我们的套路就是熟悉一种结构,遇到不同的表达方式,尝试转化成为我们熟悉的结构,进行操作 +> 图算法并不难,难点在于图有很多种表示方式,表达一张图的篇幅比较大,coding容易出错。熟悉一种结构,遇到不同的表达方式,尝试转化成为我们熟悉的结构,进行操作。 点结构的描述: -```Java -package class10; - -import java.util.ArrayList; - -// 点结构的描述 A 0 -public class Node { - // 点的编号,标识 - public int value; +```Go +// Node 图中的点元素表示 +type Node struct { + // 点的身份标识 + value int // 入度,表示有多少个点连向该点 - public int in; + in int // 出度,表示从该点出发连向别的节点多少 - public int out; - // 直接邻居:表示由自己出发,直接指向哪些节点。nexts.size==out - public ArrayList nexts; + out int + // 直接邻居:表示由自己出发,直接指向哪些节点。指向节点的总数等于out + nexts []*Node // 直接下级边:表示由自己出发的边有多少 - public ArrayList edges; - - public Node(int value) { - this.value = value; - in = 0; - out = 0; - nexts = new ArrayList<>(); - edges = new ArrayList<>(); - } + edges []*Edge } - ``` 边结构的描述: -```Java -package class10; - -// 由于任何图都可以理解为有向图,我们定义有向的边结构 -public class Edge { - // 边的权重信息 - public int weight; +```Go +// Edge 图中的边元素表示 +type Edge struct { + // 边的权重信息 + weight int // 出发的节点 - public Node from; + from *Node // 指向的节点 - public Node to; - - public Edge(int weight, Node from, Node to) { - this.weight = weight; - this.from = from; - this.to = to; - } - + to *Node } - ``` 图结构的描述: -```Java -package class10; - -import java.util.HashMap; -import java.util.HashSet; - -// 图结构 -public class Graph { - // 点的集合,编号为1的点是什么,用map - public HashMap nodes; - // 边的集合 - public HashSet edges; - - public Graph() { - nodes = new HashMap<>(); - edges = new HashSet<>(); - } +```Go +type Graph struct { + // 点的集合,编号为1的点是什么,用map + nodes map[int]*Node + // 边的集合(用hash实现set) + edges map[*Edge]string } - ``` -任意图结构的描述,向我们上述的图结构转化: - -例如,我们有一种图的描述是,变的权重,从from节点指向to节点 - -```Java -package class10; - -public class GraphGenerator { - - // matrix 所有的边 - // N*3 的矩阵 - // [weight, from节点上面的值,to节点上面的值] - public static Graph createGraph(Integer[][] matrix) { - // 定义我们的图结构 - Graph graph = new Graph(); - // 遍历给定的图结构进行转换 - for (int i = 0; i < matrix.length; i++) { - // matrix[0][0], matrix[0][1] matrix[0][2] - Integer weight = matrix[i][0]; - Integer from = matrix[i][1]; - Integer to = matrix[i][2]; - - // 我们的图结构不包含当前from节点,新建该节点 - if (!graph.nodes.containsKey(from)) { - graph.nodes.put(from, new Node(from)); - } - // 没有to节点,建立该节点 - if (!graph.nodes.containsKey(to)) { - graph.nodes.put(to, new Node(to)); - } - // 拿出我们图结构的from节点 - Node fromNode = graph.nodes.get(from); - // 拿出我们图结构的to节点 - Node toNode = graph.nodes.get(to); - // 建立我们的边结构。权重,from指向to - Edge newEdge = new Edge(weight, fromNode, toNode); - // 把to节点加入到from节点的直接邻居中 - fromNode.nexts.add(toNode); - // from的出度加1 - fromNode.out++; - // to的入度加1 - toNode.in++; - // 该边需要放到from的直接边的集合中 - fromNode.edges.add(newEdge); - // 把该边加入到我们图结构的边集中 - graph.edges.add(newEdge); - } - return graph; - } - -} -``` +任意图结构的描述,通过解析向上述的图结构进行转化。 ### 1.2.3 图的遍历 @@ -470,53 +249,45 @@ C-->E 4、直到队列变空 -> 宽度优先的思路:实质先遍历自己,再遍历自己的下一跳节点(同一层节点的顺序无需关系),再下下跳节点...... +> 宽度优先的思路:实质先遍历自己,再遍历自己的下一跳节点(同一层节点的顺序无需关心),再下下跳节点...... 我们从A点开始遍历: 1、A进队列--> Q[A];A进入Set--> S[A] -2、A出队:Q[],**打印A**;A直接邻居为BCD,都不在Set中,进入队列Q[D,C,B], 进入S[A,B,C,D] +2、A出队:Q[] , **打印A**;A直接邻居为BCD,都不在Set中,进入队列Q[D,C,B] , 进入S[A,B,C,D] -3、B出队:Q[D,C], B有CE三个邻居,C已经在Set中, 放入E, S[A,B,C,D,E],队列放E, Q[E,D,C] +3、B出队:Q[D,C], B有CDE三个邻居,C已经在Set中, 放入E, S[A,B,C,D,E],队列放E, Q[E,D,C] 4、 C出队,周而复始 -```Java -package class10; - -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Queue; - -public class Code02_BFS { - - // 从node出发,进行宽度优先遍历 - public static void bfs(Node node) { - if (node == null) { - return; - } - Queue queue = new LinkedList<>(); - // 图需要用set结构,因为图相比于二叉树有可能存在环 - // 即有可能存在某个点多次进入队列的情况 - HashSet set = new HashSet<>(); - queue.add(node); - set.add(node); - while (!queue.isEmpty()) { - Node cur = queue.poll(); - System.out.println(cur.value); - for (Node next : cur.nexts) { - // 直接邻居,没有进入过Set的进入Set和队列 - // 用set限制队列的元素,防止有环队列一直会加入元素 - if (!set.contains(next)) { - set.add(next); - queue.add(next); - } +```Go +// 从node出发,对图进行宽度优先遍历 +func (node *Node) bfs() { + if node == nil { + return + } + Queue := make([]*Node, 0) + // 图需要用set结构,因为图相比于二叉树有可能存在环 + // 即有可能存在某个点多次进入队列的情况。使用Set可以防止相同节点重复进入队列 + Set := make(map[*Node]string, 0) + Queue = append(Queue, node) + Set[node] = "" + for len(Queue) != 0 { + // 出队列 + cur := Queue[0] + Queue = Queue[1:] + fmt.Println(cur.value) + for _, next := range cur.nexts { + // 直接邻居,没有进入过Set的进入Set和队列 + // 用set限制队列的元素,防止有环队列一直会加入元素 + if _, ok := Set[next]; !ok { // Set中不存在, 则加入队列 + Set[next] = "" + Queue = append(Queue, next) } } } - } ``` @@ -530,7 +301,7 @@ public class Code02_BFS { 4、直到栈变空 -> 深度优先思路:表示从某个节点一直往下深入,知道没有路了,返回。我们的栈实质记录的是我们深度优先遍历的路径 +> 深度优先思路:表示从某个节点一直往下深入,直到没有路了,返回。我们的栈实质记录的是我们深度优先遍历的路径 我们从A点开始遍历: @@ -542,46 +313,42 @@ public class Code02_BFS { 4、 弹出D,D的直接邻居是A,A已经在栈中了。说明A-B-E-D这条路径走到了尽头。弹出D之后,当前循环结束。继续while栈不为空,重复操作 -```Java -package class10; - -import java.util.HashSet; -import java.util.Stack; - -public class Code02_DFS { +```Go +// 从node出发,对图进行深度优先遍历。借助栈 +func (node *Node) dfs() { + if node == nil { + return + } - public static void dfs(Node node) { - if (node == null) { - return; - } - Stack stack = new Stack<>(); - // Set的作用和宽度优先遍历类似,保证重复的点不要进栈 - HashSet set = new HashSet<>(); - stack.add(node); - set.add(node); - // 打印实时机是在进栈的时候 - // 同理该步可以换成其他处理逻辑,表示深度遍历处理某件事情 - System.out.println(node.value); - while (!stack.isEmpty()) { - Node cur = stack.pop(); - // 枚举当前弹出节点的后代 - for (Node next : cur.nexts) { - // 只要某个后代没进入过栈,进栈 - if (!set.contains(next)) { - // 把该节点的父亲节点重新压回栈中 - stack.push(cur); - // 再把自己压入栈中 - stack.push(next); - set.add(next); - // 打印当前节点的值 - System.out.println(next.value); - // 直接break,此时栈顶是当前next节点,达到深度优先的目的 - break; - } + stack := make([]*Node, 0) + // Set的作用和宽度优先遍历类似,保证重复的点不要进栈 + set := make(map[*Node]string, 0) + // 进栈 + stack = append(stack, node) + set[node] = "" + // 打印时机是在进栈的时候 + // 同理该步可以换成其他处理逻辑,表示深度遍历处理某件事情 + fmt.Println(node.value) + + for len(stack) != 0 { + cur := stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + // 枚举当前弹出节点的后代 + for _, next := range cur.nexts { + // 只要某个后代没进入过栈,进栈 + if _, ok := set[next]; !ok { + // 把该节点的父亲节点重新压回栈中 + stack = append(stack, cur) + // 再把自己压入栈中 + stack = append(stack, next) + set[next] = "" + // 打印当前节点的值 + fmt.Println(next.value) + // 直接break,此时栈顶是当前next节点,达到深度优先的目的 + break } } } - } ``` @@ -616,54 +383,42 @@ F-->T 拓扑排序为:A B C E F T -```Java -package class10; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; - -public class Code03_TopologySort { - - // 有向无环图,返回拓扑排序的顺序list - public static List sortedTopology(Graph graph) { - // key:某一个node - // value:该节点剩余的入度 - HashMap inMap = new HashMap<>(); - // 剩余入度为0的点,才能进这个队列 - Queue zeroInQueue = new LinkedList<>(); - - // 拿到该图中所有的点集 - for (Node node : graph.nodes.values()) { - // 初始化每个点,每个点的入度是原始节点的入度信息 - // 加入inMap - inMap.put(node, node.in); - // 由于是有向无环图,则必定有入度为0的起始点。放入到zeroInQueue - if (node.in == 0) { - zeroInQueue.add(node); - } +```Go +// sortTopology 图的拓扑排序。返回拓扑排序的顺序list +func (graph *Graph) sortTopology() []*Node { + // key:某一个node, value:该节点剩余的入度 + inMap := make(map[*Node]int) + // 剩余入度为0的点,才能进这个队列 + zeroInQueue := make([]*Node, 0) + // 拿到该图中所有的点集 + for _, node := range graph.nodes { + // 初始化每个点,每个点的入度是原始节点的入度信息 + // 加入inMap + inMap[node] = node.in + // 由于是有向无环图,则必定有入度为0的起始点。放入到zeroInQueue + if node.in == 0 { + zeroInQueue = append(zeroInQueue, node) } - - // 拓扑排序的结果,依次加入result - List result = new ArrayList<>(); - - while (!zeroInQueue.isEmpty()) { - // 该有向无环图初始入度为0的点,直接弹出放入结果集中 - Node cur = zeroInQueue.poll(); - result.add(cur); - // 该节点的下一层邻居节点,入度减一且加入到入度的map中 - for (Node next : cur.nexts) { - inMap.put(next, inMap.get(next) - 1); - // 如果下一层存在入度变为0的节点,加入到0入度的队列中 - if (inMap.get(next) == 0) { - zeroInQueue.add(next); - } + } + + // 拓扑排序的结果,依次加入result + result := make([]*Node, 0) + + for len(zeroInQueue) != 0 { + // 该有向无环图初始入度为0的点,直接弹出放入结果集中 + cur := zeroInQueue[0] + zeroInQueue = zeroInQueue[1:] + result = append(result, cur) + // 该节点的下一层邻居节点,入度减一且加入到入度的map中 + for _,next := range cur.nexts { + inMap[next] = inMap[next] - 1 + // 如果下一层存在入度变为0的节点,加入到0入度的队列中 + if inMap[next] == 0 { + zeroInQueue = append(zeroInQueue, next) } } - return result; } + return result } ``` @@ -685,117 +440,173 @@ public class Code03_TopologySort { 5、考察完所有边之后,最小生成树的集合也就得到了 -```Java -package class10; - -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.PriorityQueue; -import java.util.Set; -import java.util.Stack; - -//undirected graph only -public class Code04_Kruskal { - - // Union-Find Set 我们的并查集结构 - public static class UnionFind { - // key 某一个节点, value key节点往上的节点 - private HashMap fatherMap; - // key 某一个集合的代表节点, value key所在集合的节点个数 - private HashMap sizeMap; - - public UnionFind() { - fatherMap = new HashMap(); - sizeMap = new HashMap(); - } - - public void makeSets(Collection nodes) { - fatherMap.clear(); - sizeMap.clear(); - for (Node node : nodes) { - fatherMap.put(node, node); - sizeMap.put(node, 1); - } - } +```Go +package main - private Node findFather(Node n) { - Stack path = new Stack<>(); - while(n != fatherMap.get(n)) { - path.add(n); - n = fatherMap.get(n); - } - while(!path.isEmpty()) { - fatherMap.put(path.pop(), n); - } - return n; - } +// kruskaMST 克鲁斯卡尔最小生成树算法。返回set +func kruskaMST(graph *Graph) map[*Edge]string { + values := make([]int, 0) + for k := range graph.nodes { + values = append(values, k) + } + // 初始化一个并查集结构 + unitionSet := InitUnionSet(values) + edgesHeap := make(Edges, 0) + // 边按照权值从小到大排序,加入到堆 + for edge := range graph.edges { + edgesHeap.Push(edge) + } - public boolean isSameSet(Node a, Node b) { - return findFather(a) == findFather(b); - } + resultSet := make(map[*Edge]string) - public void union(Node a, Node b) { - if (a == null || b == null) { - return; - } - Node aDai = findFather(a); - Node bDai = findFather(b); - if (aDai != bDai) { - int aSetSize = sizeMap.get(aDai); - int bSetSize = sizeMap.get(bDai); - if (aSetSize <= bSetSize) { - fatherMap.put(aDai, bDai); - sizeMap.put(bDai, aSetSize + bSetSize); - sizeMap.remove(aDai); - } else { - fatherMap.put(bDai, aDai); - sizeMap.put(aDai, aSetSize + bSetSize); - sizeMap.remove(bDai); - } - } + // 堆不为空,弹出小根堆的堆顶 + for len(edgesHeap) != 0 { + // 假设M条边,O(logM) + edge := edgesHeap.Pop().(*Edge) + // 如果该边的左右两侧不在同一个集合中 + if !unitionSet.IsSameSet(edge.from.value, edge.to.value) { + // 要这条边 + resultSet[edge] = "" + // 联合from和to + unitionSet.Union(edge.from.value, edge.to.value) } } - + return resultSet +} - public static class EdgeComparator implements Comparator { +// Edges 边的集合。实现小根堆 +type Edges []*Edge - @Override - public int compare(Edge o1, Edge o2) { - return o1.weight - o2.weight; - } +func (es Edges) Less(i, j int) bool { + return es[i].weight <= es[j].weight +} + +func (es Edges) Len() int { + return len(es) +} + +func (es Edges) Swap(i, j int) { + es[i], es[j] = es[j], es[i] +} + +func (es *Edges) Push(v interface{}) { + *es = append(*es, v.(*Edge)) +} + +func (es *Edges) Pop() (x interface{}) { + n := len(*es) + x = (*es)[n-1] + *es = (*es)[:n-1] + return x +} +// UNode 并查集结构中的节点类型 +type UNode struct { + V int +} + +type UnionSet struct { + // 记录样本到样本代表点的关系。值到代表该值的Node节点的关系映射 + Nodes map[int]*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 []int) *UnionSet { + us := &UnionSet{} + nodes := make(map[int]*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 } - // K算法 - public static Set kruskalMST(Graph graph) { - // 先拿到并查集结构 - UnionFind unionFind = new UnionFind(); - // 该图的所有点加入到并查集结构 - unionFind.makeSets(graph.nodes.values()); - // 边按照权值从小到大排序,加入到堆 - PriorityQueue priorityQueue = new PriorityQueue<>(new EdgeComparator()); - - for (Edge edge : graph.edges) { // M 条边 - priorityQueue.add(edge); // O(logM) - } - - Set result = new HashSet<>(); - // 堆不为空,弹出小根堆的堆顶 - while (!priorityQueue.isEmpty()) { - // 假设M条边,O(logM) - Edge edge = priorityQueue.poll(); - - // 如果该边的左右两侧不在同一个集合中 - if (!unionFind.isSameSet(edge.from, edge.to)) { // O(1) - // 要这条边 - result.add(edge); - // 联合from和to - unionFind.union(edge.from, edge.to); - } + 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 } - return result; + } + return cur +} + +// IsSameSet 判断两个元素是否在同一个并查集中 +func (set *UnionSet) IsSameSet(a, b int) bool { + // 先检查a和b有没有登记 + if _, ok := set.Nodes[a]; !ok { + return false + } + if _, ok := set.Nodes[b]; !ok { + return false + } + + // 比较a的最上的代表点和b最上的代表点 + return set.FindFather(set.Nodes[a]) == set.FindFather(set.Nodes[b]) +} + +// Union 合并两个元素 +func (set *UnionSet) Union(a, b int) { + // 先检查a和b有没有都登记过 + if _, ok := set.Nodes[a]; !ok { + return + } + if _, ok := set.Nodes[b]; !ok { + return + } + + // 找到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) } } ``` @@ -813,116 +624,86 @@ public class Code04_Kruskal { 4、周而复始,直到所有点被解锁 -```Java -package class10; - -import java.util.Comparator; -import java.util.HashSet; -import java.util.PriorityQueue; -import java.util.Set; - -// undirected graph only -public class Code05_Prim { - - public static class EdgeComparator implements Comparator { - - @Override - public int compare(Edge o1, Edge o2) { - return o1.weight - o2.weight; - } - - } - - public static Set primMST(Graph graph) { - // 解锁的边进入小根堆 - PriorityQueue priorityQueue = new PriorityQueue<>(new EdgeComparator()); - - // 哪些点被解锁出来了 - HashSet nodeSet = new HashSet<>(); - // 已经考虑过的边,不要重复考虑 - Set result = new HashSet<>(); - // 依次挑选的的边在result里 - Set result = new HashSet<>(); - // 随便挑了一个点,进入循环处理完后直接break - for (Node node : graph.nodes.values()) { - // node 是开始点 - if (!nodeSet.contains(node)) { - // 开始节点保留 - nodeSet.add(node); - // 开始节点的所有邻居节点全部放到小根堆 - // 即由一个点,解锁所有相连的边 - for (Edge edge : node.edges) { - if (!edgeSet.contains(edge)) { - edgeSet.add(edge); - priorityQueue.add(edge); - } +```Go +package main + +// primMST prim算法实现图的最小生成树 +func (graph *Graph) primMST() map[*Edge]string { + // 哪些点被解锁出来了 + nodeSet := make(map[*Node]string, 0) + // 边的小根堆 + edgesHeap := make(Edges, 0) + // 已经考虑过的边,不要重复考虑 + edgeSet := make(map[*Edge]string, 0) + // 依次挑选的的边在resultSet里 + resultSet := make(map[*Edge]string, 0) + // 随便挑了一个点,进入循环处理完后直接break + + for _, node := range graph.nodes { + // node 是开始点 + if _, ok := nodeSet[node]; !ok { + // 开始节点保留 + nodeSet[node] = "" + // 开始节点的所有邻居节点全部放到小根堆 + // 即由一个点,解锁所有相连的边 + for _, edge := range node.edges { + if _, ok := edgeSet[edge]; !ok { + edgeSet[edge] = "" + // 加入小根堆 + edgesHeap.Push(edge) } - - while (!priorityQueue.isEmpty()) { - // 弹出解锁的边中,最小的边 - Edge edge = priorityQueue.poll(); - // 可能的一个新的点,from已经被考虑了,只需要看to - Node toNode = edge.to; - // 不含有的时候,就是新的点 - if (!nodeSet.contains(toNode)) { - nodeSet.add(toNode); - result.add(edge); - for (Edge nextEdge : toNode.edges) { + } + + for len(edgesHeap) != 0 { + // 弹出解锁的边中,最小的边 + edge := edgesHeap.Pop().(*Edge) + // 可能的一个新的点,from已经被考虑了,只需要看to + toNode := edge.to + // 不含有的时候,就是新的点 + if _, ok := nodeSet[toNode]; !ok { + nodeSet[toNode] = "" + resultSet[edge] = "" + for _, nextEdge := range toNode.edges { // 没加过的,放入小根堆 - if (!edgeSet.contains(edge)) { - edgeSet.add(edge); - priorityQueue.add(edge); - } + if _, ok := edgeSet[nextEdge]; !ok { + edgeSet[nextEdge] = "" + edgesHeap.Push(edge) } } } } - // 直接break意味着我们不用考虑森林的情况 - // 如果不加break我们可以兼容多个无向图的森林的生成树 - // break; } - return result; } + // 直接break意味着我们不用考虑森林的情况 + // 如果不加break我们可以兼容多个无向图的森林的生成树 + // break; + return resultSet +} - // 请保证graph是连通图 - // graph[i][j]表示点i到点j的距离,如果是系统最大值代表无路 - // 返回值是最小连通图的路径之和 - public static int prim(int[][] graph) { - int size = graph.length; - int[] distances = new int[size]; - boolean[] visit = new boolean[size]; - visit[0] = true; - for (int i = 0; i < size; i++) { - distances[i] = graph[0][i]; - } - int sum = 0; - for (int i = 1; i < size; i++) { - int minPath = Integer.MAX_VALUE; - int minIndex = -1; - for (int j = 0; j < size; j++) { - if (!visit[j] && distances[j] < minPath) { - minPath = distances[j]; - minIndex = j; - } - } - if (minIndex == -1) { - return sum; - } - visit[minIndex] = true; - sum += minPath; - for (int j = 0; j < size; j++) { - if (!visit[j] && distances[j] > graph[minIndex][j]) { - distances[j] = graph[minIndex][j]; - } - } - } - return sum; - } +// Edges 边的集合。实现小根堆 +type Edges []*Edge - public static void main(String[] args) { - System.out.println("hello world!"); - } +func (es Edges) Less(i, j int) bool { + return es[i].weight <= es[j].weight +} +func (es Edges) Len() int { + return len(es) +} + +func (es Edges) Swap(i, j int) { + es[i], es[j] = es[j], es[i] +} + +func (es *Edges) Push(v interface{}) { + *es = append(*es, v.(*Edge)) +} + +func (es *Edges) Pop() (x interface{}) { + n := len(*es) + x = (*es)[n-1] + *es = (*es)[:n-1] + return x } ``` @@ -940,225 +721,101 @@ public class Code05_Prim { 4、源点到所有的点记录如果都被拿过一遍,过程停止,最小距离表得到了 -```Java -package class10; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map.Entry; - -// 没改进之前的版本 -public class Code06_Dijkstra { - - // 返回的map表就是从from到表中key的各个的最小距离 - // 某个点不在map中记录,则from到该点位正无穷 - public static HashMap dijkstra1(Node from) { - // 从from出发到所有点的最小距离表 - HashMap distanceMap = new HashMap<>(); - // from到from距离为0 - distanceMap.put(from, 0); - // 已经求过距离的节点,存在selectedNodes中,以后再也不碰 - HashSet selectedNodes = new HashSet<>(); - // from 0 得到没选择过的点的最小距离 - Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes); - - // 得到minNode之后 - while (minNode != null) { - // 把minNode对应的距离取出,此时minNode就是桥连点 - int distance = distanceMap.get(minNode); - - // 把minNode上所有的邻边拿出来 - // 这里就是要拿到例如A到C和A到桥连点B再到C哪个距离小的距离 - for (Edge edge : minNode.edges) { - // 某条边对应的下一跳节点toNode - Node toNode = edge.to; - - // 如果关于from的distencMap中没有去toNode的记录,表示正无穷,直接添加该条 - if (!distanceMap.containsKey(toNode)) { - // from到minNode的距离加上个minNode到当前to节点的边距离 - distanceMap.put(toNode, distance + edge.weight); - - // 如果有,看该距离是否更小,更小就更新 - } else { - distanceMap.put(edge.to, - Math.min(distanceMap.get(toNode), distance + edge.weight)); - } - } - // 锁上minNode,表示from通过minNode到其他节点的最小值已经找到 - // minNode将不再使用 - selectedNodes.add(minNode); - // 再在没有选择的节点中挑选MinNode当成from的桥接点 - minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes); - } - // 最终distanceMap全部更新,返回 - return distanceMap; - } - - // 得到没选择过的点的最小距离 - public static Node getMinDistanceAndUnselectedNode( - HashMap distanceMap, - HashSet touchedNodes) { - Node minNode = null; - int minDistance = Integer.MAX_VALUE; - for (Entry entry : distanceMap.entrySet()) { - Node node = entry.getKey(); - int distance = entry.getValue(); - // 没有被选择过,且距离最小 - if (!touchedNodes.contains(node) && distance < minDistance) { - minNode = node; - minDistance = distance; - } - } - return minNode; - } - - /** - * 我们可以借助小根堆来替代之前的distanceMap。达到优化算法的目的 - * 原因是之前我们要遍历hash表选出最小距离,现在直接是堆顶元素 - * 但是我们找到通过桥节点更小的距离后,需要临时更该堆结构中元素数据 - * 所以系统提供的堆我们需要改写 - **/ - - public static class NodeRecord { - public Node node; - public int distance; - - public NodeRecord(Node node, int distance) { - this.node = node; - this.distance = distance; - } - } - - // 自定义小根堆结构 - // 需要提供add元素的方法,和update元素的方法 - // 需要提供ignore方法,表示我们已经找到from到某节点的最短路径 - // 再出现from到该节点的其他路径距离,我们直接忽略 - public static class NodeHeap { - private Node[] nodes; // 实际的堆结构 - // key 某一个node, value 上面堆中的位置 - // 如果节点曾经进过堆,现在不在堆上,则node对应-1 - // 用来找需要ignore的节点 - private HashMap heapIndexMap; - // key 某一个节点, value 从源节点出发到该节点的目前最小距离 - private HashMap distanceMap; - private int size; // 堆上有多少个点 - - public NodeHeap(int size) { - nodes = new Node[size]; - heapIndexMap = new HashMap<>(); - distanceMap = new HashMap<>(); - size = 0; - } - - // 该堆是否空 - public boolean isEmpty() { - return size == 0; - } - - // 有一个点叫node,现在发现了一个从源节点出发到达node的距离为distance - // 判断要不要更新,如果需要的话,就更新 - public void addOrUpdateOrIgnore(Node node, int distance) { - // 如果该节点在堆上,就看是否需要更新 - if (inHeap(node)) { - distanceMap.put(node, Math.min(distanceMap.get(node), distance)); - // 该节点进堆,判断是否需要调整 - insertHeapify(node, heapIndexMap.get(node)); - } - // 如果没有进入过堆。新建,进堆 - if (!isEntered(node)) { - nodes[size] = node; - heapIndexMap.put(node, size); - distanceMap.put(node, distance); - insertHeapify(node, size++); - } - // 如果不在堆上,且进来过堆上,什么也不做,ignore - } +```Go +package main - // 弹出from到堆顶节点的元素,获取到该元素的最小距离,再调整堆结构 - public NodeRecord pop() { - NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0])); - // 把最后一个元素放在堆顶,进行heapify - swap(0, size - 1); - heapIndexMap.put(nodes[size - 1], -1); - distanceMap.remove(nodes[size - 1]); - // free C++同学还要把原本堆顶节点析构,对java同学不必 - nodes[size - 1] = null; - heapify(0, --size); - return nodeRecord; - } - - private void insertHeapify(Node node, int index) { - while (distanceMap.get(nodes[index]) - < distanceMap.get(nodes[(index - 1) / 2])) { - swap(index, (index - 1) / 2); - index = (index - 1) / 2; - } - } +import "math" - private void heapify(int index, int size) { - int left = index * 2 + 1; - while (left < size) { - int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left]) - ? left + 1 - : left; - smallest = distanceMap.get(nodes[smallest]) - < distanceMap.get(nodes[index]) ? smallest : index; - if (smallest == index) { - break; - } - swap(smallest, index); - index = smallest; - left = index * 2 + 1; - } - } +// Node 图中的点元素表示 +type Node struct { + // 点的身份标识 + value int + // 入度,表示有多少个点连向该点 + in int + // 出度,表示从该点出发连向别的节点多少 + out int + // 直接邻居:表示由自己出发,直接指向哪些节点。指向节点的总数等于out + nexts []*Node + // 直接下级边:表示由自己出发的边有多少 + edges []*Edge +} - // 判断node是否进来过堆 - private boolean isEntered(Node node) { - return heapIndexMap.containsKey(node); - } +// Edge 图中的边元素表示 +type Edge struct { + // 边的权重信息 + weight int + // 出发的节点 + from *Node + // 指向的节点 + to *Node +} - // 判断某个节点是否在堆上 - private boolean inHeap(Node node) { - return isEntered(node) && heapIndexMap.get(node) != -1; - } +// Graph 图结构 +type Graph struct { + // 点的集合,编号为1的点是什么,用map + nodes map[int]*Node + // 边的集合(用hash实现set) + edges map[*Edge]string +} - private void swap(int index1, int index2) { - heapIndexMap.put(nodes[index1], index2); - heapIndexMap.put(nodes[index2], index1); - Node tmp = nodes[index1]; - nodes[index1] = nodes[index2]; - nodes[index2] = tmp; - } +// dijkstra算法-图的最短路径算法 +// 给定一个图的节点,返回这个节点到图的其他点的最短距离 +// 某个点不在map中记录,则from到该点位正无穷 +func dijkstra(from *Node) map[*Node]int { + // 从from出发到所有点的最小距离表 + distanceMap := make(map[*Node]int, 0) + // from到from距离为0 + distanceMap[from] = 0 + // 已经求过距离的节点,存在selectedNodes中,不会再被选中记录 + selectedNodesSet := make(map[*Node]string) + // from 0 得到没选择过的点的最小距离 + minNode := getMinDistanceAndUnselectedNode(distanceMap, selectedNodesSet) + // 得到minNode之后 + for minNode != nil { + // 把minNode对应的距离取出,此时minNode就是桥连点 + distance := distanceMap[minNode] + // 把minNode上所有的邻边拿出来 + // 这里就是要拿到例如A到C和A到桥连点B再到C哪个距离小的距离 + for _, edge := range minNode.edges { + // 某条边对应的下一跳节点toNode + toNode := edge.to + // 如果关于from的distencMap中没有去toNode的记录,表示正无穷,直接添加该条 + if _, ok := distanceMap[toNode]; !ok { + // from到minNode的距离加上个minNode到当前to节点的边距离 + distanceMap[toNode] = distance + edge.weight + } else { // 如果有,看该距离是否更小,更小就更新 + minDistance := int(math.Min(float64(distanceMap[toNode]), float64(distance + edge.weight))) + distanceMap[edge.to] = minDistance + } + } + // 锁上minNode,表示from通过minNode到其他节点的最小值已经找到 + // minNode将不再使用 + selectedNodesSet[minNode] = "" + // 再在没有选择的节点中挑选MinNode当成from的桥接点 + minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodesSet) } + // 最终distanceMap全部更新,返回 + return distanceMap +} - // 使用自定义小根堆,改进后的dijkstra算法 - // 从from出发,所有from能到达的节点,生成到达每个节点的最小路径记录并返回 - public static HashMap dijkstra2(Node from, int size) { - // 申请堆 - NodeHeap nodeHeap = new NodeHeap(size); - // 在堆上添加from节点到from节点距离为0 - nodeHeap.addOrUpdateOrIgnore(from, 0); - // 最终的结果集 - HashMap result = new HashMap<>(); - while (!nodeHeap.isEmpty()) { - // 每次在小根堆弹出堆顶元素 - NodeRecord record = nodeHeap.pop(); - // 拿出的节点 - Node cur = record.node; - // from到该节点的距离 - int distance = record.distance; - // 以此为桥接点,找是否有更小的距离到该节点的其他to节点 - // addOrUpdateOrIgnore该方法保证如果from到to的节点没有,就add - // 如果有,看是否需要Ignore,如果不需要Ignore且更小,就Update - for (Edge edge : cur.edges) { - nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance); - } - result.put(cur, distance); +// getMinDistanceAndUnselectedNode 得到没选择过的点的最小距离 +func getMinDistanceAndUnselectedNode(distanceMap map[*Node]int, selectedNodesSet map[*Node]string) *Node { + var minNode *Node = nil + minDistance := math.MaxInt + for node,distance := range distanceMap { + // 没有被选择过,且距离最小 + if _, ok := selectedNodesSet[node]; !ok && distance < minDistance { + minNode = node + minDistance = distance } - return result; } - + return minNode } + +// 我们可以借助小根堆来替代之前的distanceMap。达到优化算法的目的 +// 原因是之前我们要遍历hash表选出最小距离,现在直接是堆顶元素 +// 但是我们找到通过桥节点更小的距离后,需要临时更该堆结构中元素数据 +// 所以系统提供的堆我们需要改写。略 ``` #### 1.2.6.2 floyd算法 diff --git "a/11-\346\232\264\345\212\233\351\200\222\345\275\222\344\270\216\345\212\250\346\200\201\350\247\204\345\210\222.md" "b/11-\346\232\264\345\212\233\351\200\222\345\275\222\344\270\216\345\212\250\346\200\201\350\247\204\345\210\222.md" new file mode 100644 index 0000000..c824b8c --- /dev/null +++ "b/11-\346\232\264\345\212\233\351\200\222\345\275\222\344\270\216\345\212\250\346\200\201\350\247\204\345\210\222.md" @@ -0,0 +1,690 @@ +[TOC] +# 1 暴力递归、动态规划 + +## 1.1 暴力递归思维 + +**暴力递归实质就是尝试** + +> 概念解释: + +> 回溯-表示大问题被拆解为小问题,小问题返回给大问题信息,就是回溯 + +> 分治:大问题被拆解成小的子问题,就是分治 + +1、把问题转化为规模缩小了的同类问题的子问题 + +2、有明确的不需要继续进行递归的条件(base case) + +3、有当得到了子问题的结果之后的决策过程 + +4、不记录每个子问题的解(如果记录每个子问题的解,就是我们熟悉的动态规划) + +### 1.1.1 暴力递归下的尝试 + +#### 1.1.1.1 例一:汉诺塔问题 + +打印n层汉诺塔从最左边移动到最右边的全部过程 + +> 汉诺塔圆盘移动,如果杆子上没有圆盘,可以移动到该杆,如果有圆盘则必须移动比该圆盘小的圆盘到该圆盘上 + +> 思路1:1、先想办法把1到N-1层圆盘移动到中间杆 2、再把N层的圆盘移动到最右侧的杆上 3、把1到N-1个圆盘从中间杆移动到最右侧。结束 + +> 思路2:忘掉左中右,理解为从from移动到to,from和to都有可能是左中右。所以定义from,to,other三个杆子。1、把1到N-1移动到other上。2、把第N层移动到to上。3、把1到N层从other移动到to上。结束 + +> 思路3:递归改非递归实现 + +> N层汉诺塔,从左移动到右最优步数是2^N - 1 步。递归公式 T(N) = T(N-1) + 1 + T(N-1)。化简为等比数列,高中数学内容 + +尝试是有优劣之分的,譬如思路1和思路二。在动态规划章节,可以用动态规划优化我们的尝试到最优版本 + +```Go +package main + +import "fmt" + +// hanoiV1 汉诺塔问题 递归版本1 +func hanoiV1(n int) { + leftToRight(n) +} + +// leftToRight 请把1~N层圆盘 从左 -> 右 +func leftToRight(n int) { + if n == 1 { + fmt.Println("Move 1 from left to right") + return + } + + leftToMid(n -1) + fmt.Println(fmt.Sprintf("Move %d from left to right", n)) + midToRight(n - 1) +} + +// leftToMid 请把1~N层圆盘 从左 -> 中 +func leftToMid(n int) { + if n == 1 { + fmt.Println("Move 1 from left to mid") + return + } + + leftToRight(n - 1) + fmt.Println(fmt.Sprintf("Move %d from left to mid", n)) + rightToMid(n - 1) +} + +func rightToMid(n int) { + if n == 1 { + fmt.Println("Move 1 from right to mid") + return + } + rightToLeft(n - 1) + fmt.Println(fmt.Sprintf("Move %d from right to mid", n)) + leftToMid(n - 1) +} + +func midToRight(n int) { + if n == 1 { + fmt.Println("Move 1 from mid to right") + return + } + midToLeft(n - 1) + fmt.Println(fmt.Sprintf("Move %d from mid to right", n)) + leftToRight(n - 1) +} + +func midToLeft(n int) { + if n == 1 { + fmt.Println("Move 1 from mid to left") + return + } + midToRight(n - 1) + fmt.Println(fmt.Sprintf("Move %d from mid to left", n)) + rightToLeft(n - 1) +} + +func rightToLeft(n int) { + if n == 1 { + fmt.Println("Move 1 from right to left") + return + } + rightToMid(n - 1) + fmt.Println(fmt.Sprintf("Move %d from right to left", n)) + midToLeft(n - 1) +} + +// 思路二:暴力递归 版本2 from to other +func hanoiV2(n int) { + if n > 0 { + f(n, "left", "right", "mid"); + } +} + +// 1~i 圆盘 目标是from -> to, other是另外一个 +func f(n int, from, to, other string) { + if n == 1 { // base + fmt.Println(fmt.Sprintf("Move 1 from %s to %s", from, to)) + } else { + f(n - 1, from, other, to) + fmt.Println(fmt.Sprintf("Move %d from %s to %s", n, from, to)) + f(n - 1, other, to, from) + } +} +``` + +#### 1.1.1.2 例二:字符串子序列问题 + +1、打印一个字符串的全部子序列 + +> 子串:必须是连续的,用for循环就行 + +> 子序列:比子串自由,在原始序列的基础上,以此拿字符但是可以不连续 + +```Go +package main + +import "fmt" + +// 子序列问题 递归版本1 打印一个字符串的全部子序列 +func subsV1(s string) []string { + chars := []byte(s) + path := "" + ans := make([]string, 0) + processV1(chars, 0, ans, path) + return ans +} + +// str固定,不变 +// index此时来到的位置, 要 or 不要 +// 如果index来到了str中的终止位置,把沿途路径所形成的答案,放入ans中 +// 之前做出的选择,就是沿途路径path +func processV1(chars []byte, index int, ans []string, path string) { + if index == len(chars) { + ans = append(ans, path) + return + } + + no := path + processV1(chars, index + 1, ans, no) + yes := fmt.Sprintf("%s%s", path, string(chars[index])) + processV1(chars, index + 1, ans, yes) +} +``` + + + +2、打印一个字符串的全部子序列,要求不要出现重复字面值的子序列 + +> 比如aaabcccc就会得到很多相同字面值的子序列,我们把重复字面值的子序列只要一个 + +```Go +package main + +import "fmt" + +// 子序列问题 版本2 打印一个字符串的全部子序列,要求不要出现重复字面值的子序列 +func subsV2(s string) []string { + chars := []byte(s) + path := "" + set := make(map[string]string, 0) + processV2(chars, 0, set, path) + ans := make([]string, 0) + for curK := range set { + ans = append(ans, curK) + } + return ans +} + +func processV2(chars []byte, index int, set map[string]string, path string) { + if index == len(chars) { + set[path] = "" + return + } + no := path + processV2(chars, index + 1, set, no) + yes := fmt.Sprintf("%s%s", path, string(chars[index])) + processV2(chars, index + 1, set, yes) +} +``` + +#### 1.1.1.3 例四:字符串全排列问题 + +1、打印一个字符串的全部排列,process + +2、打印一个字符串的全部排列,要求不要出现重复的排列.process2。 + +方法1,可以用HashSet最后去重,该方式是把递归的所有结果进行筛选。 + +方法2可以抛弃重复元素,例如a在0位置已经尝试完毕,再有一个元素也是a要到0位置,那么禁止,该方法是递归的时候事先判断要不要进行下一步递归,更快一点。该方法又叫分支限界 + + +```Go +package main + +// 字符串全排列问题 +func permutation(str string) []string { + res := make([]string, 0) + if len(str) == 0 { + return res + } + + chs := []byte(str) + processPermutation(chs, 0, res) + return res +} + +// str[0..i-1]已经做好决定的 +// str[i...]都有机会来到i位置 +// i到达终止位置,str当前的样子,就是一种结果 -> ans +func processPermutation(str []byte, i int, ans []string) { + // i来到终点,返回该种答案 + if i == len(str) { + ans = append(ans, string(str)) + } + // 如果i没有终止,i... 都可以来到i位置 + for j := i; j < len(str); j++ { // i后面所有的字符都有机会来到i位置 + str[i], str[j] = str[j], str[i] // swap + processPermutation(str, i + 1, ans) + // 恢复交换之前的现场 + str[i], str[j] = str[j], str[i] // swap + } +} +``` + + +```Go +package main + +// 字符串全排列问题无重复 +func permutationNoRepeat(str string) []string { + res := make([]string, 0) + if len(str) == 0 { + return res + } + chs := []byte(str) + processPermutationNoRepeat(chs, 0, res) + return res +} + +// str[0..i-1]已经做好决定的 +// str[i...]都有机会来到i位置 +// i终止位置,str当前的样子,就是一种结果 -> ans +func processPermutationNoRepeat(str []byte, i int, ans []string) { + if i == len(str) { + ans = append(ans, string(str)) + return + } + + visit := make([]bool, 0) // visit[0 1 .. 25] 代表a-z的字符有没有在当前出现过 + // i右边的字符都有机会 + for j := i; j < len(str); j++ { + // str[j] = 'a' -> 0 visit[0] -> 'a' + + // str[j] = 'z' -> 25 visit[25] -> 'z' + // 如果没出现过就没有机会 + if !visit[str[j] - 'a'] { + visit[str[j] - 'a'] = true + str[i], str[j] = str[j], str[i] // swap + processPermutationNoRepeat(str, i + 1, ans) + str[i], str[j] = str[j], str[i] // swap + } + } +} +``` + +## 1.2 动态规划模型 + +> 以下代码有些已经实现了动态规划,可以看下一节详细解释。 + + +### 1.2.1 从左往右尝试模型 + +#### 1.2.1.1 数字字符转化问题 + +> 注:facebook面试题 + +1、规定1和A对应,2和B对应,3和C对应...。那么一个数字字符比如"111"就可以转化为:"AAA","KA","AK"。 + +给定一个只有数字字符组成的字符串str,返回有多少种转化结果 + +> 思路:根据从左往右,我们划分多大,来尝试,比如111,我们尝试一个1,为"A",剩下两个1去继续尝试。如果我们两个1尝试,就是"K"。三个1超过26字符,无法尝试。继续如此周而复始 + +```Go +package main + +import "fmt" + +// 数字字符转化问题。1对应A,B对应2...; 111可以转化为AAA、KA、AK。所以有三种转化 +func number(str string) int { + if len(str) == 0 { + return 0 + } + + // i初始为0,表示0位置往后有多少中转化结果 + return processConvert([]byte(str), 0) +} + +// str[0...i-1]已经转化完了,固定了 +// i之前的位置,如何转化已经做过决定了, 不用再关心 +// i... 有多少种转化的结果 +func processConvert(chars []byte, i int) int { + // i和字符串长度一样大,右侧没有字符了。找到1中转化是0~n-1位置的转化 + if i == len(chars) { // base case + return 1 + } + + // i之前的决策,让当前i位置单独面对一个0字符,那么之前决策错误,返回0 + // 例如10先转化为A,2位置是0字符无法转化,当前决策无效 + // 而10直接转化为J,直接到终止位置,返回一种转化J + if chars[i] == '0' { + return 0 + } + + // i位置如果是1或者2,有可能和下一个位置共同转化,因为字符数为0~26 + // 反之3~9超过26不需要决策 + + // str[i] 如果是1,总是有两个选择,因为最大为19,不超过26 + if chars[i] == '1' { + res := processConvert(chars, i+1) + if i+1 < len(chars) { + res += processConvert(chars, i+2) + } + return res + } + + // str[i] 如果是2,那么有可能有两种选择,需要看是否超过26 + if chars[i] == '2' { + res := processConvert(chars, i+1) + if i+1 < len(chars) && (chars[i+1] >= '0' && chars[i+1] <= '6') { + res += processConvert(chars, i+2) // (i和i+1)作为单独的部分,后续有多少种方法 + } + return res + } + + // // str[i] 在3~9的位置,下个位置必须决策一种选择 + return processConvert(chars, i+1) +} + +// dbNumber 动态规划解法 +func dbNumber(str string) int { + if len(str) == 0 { + return 0 + } + + chars := []byte(str) + N := len(str) + dp := make([]int, 0, N+1) + dp[N] = 1 + for i := N - 1; i >= 0; i-- { + if chars[i] == '0' { + dp[i] = 0 + } else if chars[i] == '1' { + dp[i] = dp[i+1] + if i+1 < N { + dp[i] += dp[i+2] + } + } else if chars[i] == '2' { + dp[i] = dp[i+1] + if i+1 < N && (chars[i+1] >= '0' && chars[i+1] <= '6') { + dp[i] += dp[i+2] + } + } else { + dp[i] = dp[i + 1] + } + } + return dp[0] +} + +func main() { + fmt.Println(number("111")) +} +``` + +#### 1.2.1.2 背包价值问题 + +2、给定两个长度都为N的数组weights和values,weight[i]和values[i]分别代表i号物品的重量和价值。 + +给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少? + +```Go +package main + +import ( + "fmt" + "math" +) + +// maxBagValue 背包最大价值问题。 +func maxBagValue(w, v []int, bag int) int { + return processRest(w, v, 0, bag) +} + +// 只剩下rest的空间了, +// index...货物自由选择,但是剩余空间不要小于0 +// 返回 index...货物能够获得的最大价值 +func processRest(w, v []int, index, rest int) int { + // base case + if rest < 0 { + return -1 + } + + // rest >=0。index来到终止位置,当前返回0价值 + // base case 2 + if index == len(w) { + return 0 + } + + // 有货也有空间。当前index不选择,得到p1总价值 + p1 := processRest(w, v, index+1, rest) + p2 := -1 + // 选择了index位置,剩余空间减去当前重量 + p2Next := processRest(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))) +} + +// maxBagValueDp 背包问题动态规划解法 +func maxBagValueDp(w, v []int, bag int) int { + N := len(w) + + // m行 n列。golang数组只能常量初始化,这里动态构造切片 + m,n := N+1, bag+1 + dp := make([][]int, m) + for i := range dp { + dp[i] = make([]int, n) + } + + for index := N - 1; index >= 0; index-- { + for rest := 1; rest <= bag; rest++ { + dp[index][rest] = dp[index+1][rest] + if rest >= w[index] { + dp[index][rest] = int(math.Max(float64(dp[index][rest]), float64(v[index]+dp[index+1][rest-w[index]]))) + } + } + } + return dp[0][bag] +} + +func main() { + w := []int{3, 2, 4, 7} + v := []int{5, 6, 3, 19} + bag := 11 + fmt.Println(maxBagValue(w, v, bag)) + fmt.Println(maxBagValueDp(w, v, bag)) +} +``` + +### 1.2.2 范围上的尝试模型 + +#### 1.2.2.1 玩家抽取纸牌问题 + +给定一个整形数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B依次拿走每张纸牌。规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数 + +> 绝顶聪明学术上的解释:双方玩家都会使得对方玩家在当前单独改变策略时,不会获得更大的收益。 + +```Go +package main + +import "math" + +// CardsInLineWin 给定纸牌,返回A先手的情况下,最后获胜者的分数 +func CardsInLineWin(arr []int) int { + if len(arr) == 0 { + return 0 + } + // 先手在0~length-1和后手在0~length-1上,谁分数大就是获胜者的分数 + return int(math.Max(float64(firstHand(arr, 0, len(arr)-1)), float64(secondHand(arr, 0, len(arr)-1)))) +} + +// L....R 先手函数 +// F S L+1..R +// L..R-1 +func firstHand(arr []int, L int, R int) int { + // base case 当只剩一张牌,且是先手 + if L == R { + return arr[L] + } + + // 当前是先手,选择最好的 + return int(math.Max(float64(arr[L]+secondHand(arr, L+1, R)), float64(arr[R]+secondHand(arr, L, R-1)))) +} + +// 后手函数 +// arr[L..R] +func secondHand(arr []int, L int, R int) int { + // base case 当只剩一张牌,且为后手 + if L == R { + return 0 + } + // 当前是后手,好的被绝顶聪明的先手选走了 + // 相当于是先手的决策剩下的当前牌,留下最差的min + return int(math.Min(float64(firstHand(arr, L+1, R)), float64(firstHand(arr, L, R-1)))) +} + +func CardsInLineWinDp(arr []int) int { + if len(arr) == 0 { + return 0 + } + + N := len(arr) + // m行 n列。golang数组只能常量初始化,这里动态构造切片 + m, n := N, N + dpf := make([][]int, m) + dps := make([][]int, m) + for i := range dpf { + dpf[i] = make([]int, n) + dps[i] = make([]int, n) + } + + for i := 0; i < N; i++ { + dpf[i][i] = arr[i] + } + + // dps[i][i] = 0 + for i := 1; i < N; i++ { + L := 0 + R := i + for L < N && R < N { + dpf[L][R] = int(math.Max(float64(arr[L]+dps[L+1][R]), float64(arr[R]+dps[L][R-1]))) + dps[L][R] = int(math.Min(float64(dpf[L+1][R]), float64(dpf[L][R-1]))) + L++ + R++ + } + } + return int(math.Max(float64(dpf[0][N-1]), float64(dps[0][N-1]))) +} +``` +#### 1.2.2.2 N皇后问题 + +N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行,不同列,也不在同一条斜线上。 + +给定一个整数n,返回n皇后的摆法有多少种。 + +n=1,返回1 + +n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0 + +n=8,返回92 +> 最优的N皇后的尝试方法,时间复杂度为N^N,但是process2是用位运算加速了常数项的时间 + +```Go +package main + +import ( + "fmt" + "math" + "time" +) + +// NQueensNum1 n皇后摆放问题解法1 +func NQueensNum1(n int) int { + if n < 1 { + return 0 + } + + // len是0 cap是n; golang中append是在扩大len,当len超过数组的cap时,会触发cap扩容,指向新开辟的底层数组 + // !!! 如果指定len和cap相同,注意不要使用append触发数组扩容 + record := make([]int, n, n) + return NQueensProcess1(0, record, n) +} + +// NQueensProcess1 潜台词:record[0..i-1]的皇后,任何两个皇后一定都不共行、不共列,不共斜线 +// 目前来到了第i行,在i行准备放皇后 +// record[0..i-1]表示之前的行,放了的皇后位置 +// n代表整体一共有多少行 0~n-1行 +// 返回值是,摆完所有的皇后,合理的摆法有多少种 +// 尝试过程 +func NQueensProcess1(i int, record []int, n int) int { + if i == n { // // 终止行 + return 1 + } + // 没有到终止位置,还有皇后要摆 + res := 0 + // 当前行在i行,尝试i行所有的列 -> j + for j := 0; j < n; j++ { + // 当前i行的皇后,放在j列,会不会和之前(0..i-1)的皇后,不共行共列共斜线, + // 如果是,认为有效,当前可以摆在j列的位置 + // 如果不是,认为无效 + if isValid(record, i, j) { + // 当前存在i行的有效值为j列位置 + record[i] = j + res += NQueensProcess1(i+1, record, n) + } + } + return res +} + +// record[0..i-1]你需要看,record[i...]不需要看 +// 返回i行皇后,放在了j列,是否有效 +// a行b列的皇后,和c行d列的皇后会不会冲突,coding的条件是不共行 +// 共列的话b==d,共斜线的话|a-c|==|b-d| +func isValid(record []int, i, j int) bool { + // 之前的某个k行的皇后 + for k := 0; k < i; k++ { + if j == record[k] || math.Abs(float64(record[k]-j)) == math.Abs(float64(i-k)) { + return false + } + } + return true +} + +// NQueensNum2 n皇后摆放问题解法2。 +// 请不要超过32皇后问题 +func NQueensNum2(n int) int { + if n < 1 || n > 32 { + return 0 + } + + limit := 0 + if n == 32 { + limit = -1 + } else { + limit = (1 << n) - 1 + } + + return NQueensProcess2(limit, 0, 0, 0) +} + +// NQueensProcess2 limit 划定了问题的规模 -> 固定 +// 用位运算加速常数项时间 +// colLim 列的限制,1的位置不能放皇后,0的位置可以 +// leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以 +// rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以 +func NQueensProcess2(limit, colLim, leftDiaLim, rightDiaLim int) int { + if colLim == limit { // base case + return 1 + } + + // 所有可以放皇后的位置,都在pos上 + // colLim | leftDiaLim | rightDiaLim -> 总限制 + // ~ (colLim | leftDiaLim | rightDiaLim) -> 左侧的一坨0干扰,右侧每个1,可尝试 + + // 把左侧的去反后的一坨1,移除掉 + pos := limit & (^(colLim | leftDiaLim | rightDiaLim)) + mostRightOne := 0 + res := 0 + for pos != 0 { + // 提取出pos中,最右侧的1来,剩下位置都是0 + mostRightOne = pos & (^pos + 1) + + pos = pos - mostRightOne + // int(uint(rightDiaLim | mostRightOne) >> 1 相当于java (rightDiaLim | mostRightOne) >>> 1 表示无符号右移 + res += NQueensProcess2(limit, colLim | mostRightOne, (leftDiaLim | mostRightOne) << 1, int(uint(rightDiaLim | mostRightOne) >> 1)) + } + return res +} + +func main() { + n := 5 + start := time.Now().Unix() + fmt.Println(NQueensNum1(n)) + end := time.Now().Unix() + fmt.Println(end - start) + + start2 := time.Now().Unix() + fmt.Println(NQueensNum2(n)) + end2 := time.Now().Unix() + fmt.Println(end2 - start2) +} +``` diff --git "a/11-\346\232\264\345\212\233\351\200\222\345\275\222\346\200\235\347\273\264\343\200\201\345\212\250\346\200\201\350\247\204\345\210\222\346\200\235\347\273\264.md" "b/11-\346\232\264\345\212\233\351\200\222\345\275\222\346\200\235\347\273\264\343\200\201\345\212\250\346\200\201\350\247\204\345\210\222\346\200\235\347\273\264.md" deleted file mode 100644 index 548f49e..0000000 --- "a/11-\346\232\264\345\212\233\351\200\222\345\275\222\346\200\235\347\273\264\343\200\201\345\212\250\346\200\201\350\247\204\345\210\222\346\200\235\347\273\264.md" +++ /dev/null @@ -1,915 +0,0 @@ -[TOC] -# 1 暴力递归、动态规划 - -## 1.1 暴力递归思维 - -**==暴力递归实质就是尝试==** - -> 概念解释: - -> 回溯-表示大问题被拆解为小问题,小问题返回给大问题信息,就是回溯 - -> 分治:大问题被拆解成小的子问题,就是分治 - -1、把问题转化为规模缩小了的同类问题的子问题 - -2、有明确的不需要继续进行递归的条件(base case) - -3、有当得到了子问题的结果之后的决策过程 - -4、不记录每个子问题的解(如果记录每个子问题的解,就是我们熟悉的动态规划) - -### 1.1.1 暴力递归下的尝试 - -#### 1.1.1.1 例一:汉诺塔问题 - -打印n层汉诺塔从最左边移动到最右边的全部过程 - -> 汉诺塔圆盘移动,如果杆子上没有圆盘,可以移动到该杆,如果有圆盘则必须移动比该圆盘小的圆盘到该圆盘上 - -> 思路1:1、先想办法把1到N-1层圆盘移动到中间杆 2、再把N层的圆盘移动到最右侧的杆上 3、把1到N-1个圆盘从中间杆移动到最右侧。结束 - -> 思路2:忘掉左中右,理解为从from移动到to,from和to都有可能是左中右。所以定义from,to,other三个杆子。1、把1到N-1移动到other上。2、把第N层移动到to上。3、把1到N层从other移动到to上。结束 - -> 思路3:递归改非递归实现 - -> N层汉诺塔,从左移动到右最优步数是2^N - 1 步。递归公式 T(N) = T(N-1) + 1 + T(N-1)。化简为等比数列,高中数学内容 - -尝试是有优劣之分的,譬如思路1和思路二。在动态规划章节,可以用动态规划优化我们的尝试到最优版本 - -```Java -package class11; - -import java.util.Stack; - -public class Code01_Hanoi { - - // 按照思路1的方法 - public static void hanoi1(int n) { - leftToRight(n); - } - - // 请把1~N层圆盘 从左 -> 右 - public static void leftToRight(int n) { - if (n == 1) { - System.out.println("Move 1 from left to right"); - return; - } - leftToMid(n - 1); - System.out.println("Move " + n + " from left to right"); - midToRight(n - 1); - } - - // 请把1~N层圆盘 从左 -> 中 - public static void leftToMid(int n) { - if (n == 1) { - System.out.println("Move 1 from left to mid"); - return; - } - leftToRight(n - 1); - System.out.println("Move " + n + " from left to mid"); - rightToMid(n - 1); - } - - public static void rightToMid(int n) { - if (n == 1) { - System.out.println("Move 1 from right to mid"); - return; - } - rightToLeft(n - 1); - System.out.println("Move " + n + " from right to mid"); - leftToMid(n - 1); - } - - public static void midToRight(int n) { - if (n == 1) { - System.out.println("Move 1 from mid to right"); - return; - } - midToLeft(n - 1); - System.out.println("Move " + n + " from mid to right"); - leftToRight(n - 1); - } - - public static void midToLeft(int n) { - if (n == 1) { - System.out.println("Move 1 from mid to left"); - return; - } - midToRight(n - 1); - System.out.println("Move " + n + " from mid to left"); - rightToLeft(n - 1); - } - - public static void rightToLeft(int n) { - if (n == 1) { - System.out.println("Move 1 from right to left"); - return; - } - rightToMid(n - 1); - System.out.println("Move " + n + " from right to left"); - midToLeft(n - 1); - } - - // 思路二:暴力递归 from to other - public static void hanoi2(int n) { - if (n > 0) { - func(n, "left", "right", "mid"); - } - } - - // 1~i 圆盘 目标是from -> to, other是另外一个 - public static void func(int N, String from, String to, String other) { - if (N == 1) { // base - System.out.println("Move 1 from " + from + " to " + to); - } else { - func(N - 1, from, other, to); - System.out.println("Move " + N + " from " + from + " to " + to); - func(N - 1, other, to, from); - } - } - - public static class Record { - public boolean finish1; - public int base; - public String from; - public String to; - public String other; - - public Record(boolean f1, int b, String f, String t, String o) { - finish1 = false; - base = b; - from = f; - to = t; - other = o; - } - } - - // 思路三:非递归实现 - public static void hanoi3(int N) { - if (N < 1) { - return; - } - Stack stack = new Stack<>(); - stack.add(new Record(false, N, "left", "right", "mid")); - while (!stack.isEmpty()) { - Record cur = stack.pop(); - if (cur.base == 1) { - System.out.println("Move 1 from " + cur.from + " to " + cur.to); - if (!stack.isEmpty()) { - stack.peek().finish1 = true; - } - } else { - if (!cur.finish1) { - stack.push(cur); - stack.push(new Record(false, cur.base - 1, cur.from, cur.other, cur.to)); - } else { - System.out.println("Move " + cur.base + " from " + cur.from + " to " + cur.to); - stack.push(new Record(false, cur.base - 1, cur.other, cur.to, cur.from)); - } - } - } - } - - public static void main(String[] args) { - int n = 3; - hanoi1(n); - System.out.println("============"); - hanoi2(n); - System.out.println("============"); - hanoi3(n); - } - -} -``` - -#### 1.1.1.2 例二:字符串子序列问题 - -1、打印一个字符串的全部子序列 - -> 子串:必须是连续的,用for循环就行 - -> 子序列:比子串自由,在原始序列的基础上,以此拿字符但是可以不连续 - -- 见process1方法代码 - - - -2、打印一个字符串的全部子序列,要求不要出现重复字面值的子序列 - -> 比如aaabcccc就会得到很多相同字面值的子序列,我们把重复字面值的子序列只要一个 - -- 见process2方法代码 - -```Java -package class11; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -public class Code02_PrintAllSubsquences { - - public static List subs(String s) { - char[] str = s.toCharArray(); - String path = ""; - List ans = new ArrayList<>(); - process1(str, 0, ans, path); - return ans; - } - - // str固定,不变 - // index此时来到的位置, 要 or 不要 - // 如果index来到了str中的终止位置,把沿途路径所形成的答案,放入ans中 - // 之前做出的选择,就是沿途路径path - public static void process1(char[] str, int index, List ans, String path) { - if (index == str.length) { - ans.add(path); - return; - } - String no = path; - process1(str, index + 1, ans, no); - String yes = path + String.valueOf(str[index]); - process1(str, index + 1, ans, yes); - } - - public static List subsNoRepeat(String s) { - char[] str = s.toCharArray(); - String path = ""; - HashSet set = new HashSet<>(); - process2(str, 0, set, path); - List ans = new ArrayList<>(); - for (String cur : set) { - ans.add(cur); - } - return ans; - } - - // str index 用set去重 - public static void process2(char[] str, int index, - HashSet set, String path) { - if (index == str.length) { - set.add(path); - return; - } - String no = path; - process2(str, index + 1, set, no); - String yes = path + String.valueOf(str[index]); - process2(str, index + 1, set, yes); - } - - public static void main(String[] args) { - String test = "aacc"; - List ans1 = subs(test); - List ans2 = subsNoRepeat(test); - - for (String str : ans1) { - System.out.println(str); - } - System.out.println("================="); - for (String str : ans2) { - System.out.println(str); - } - System.out.println("================="); - - } - -} -``` - -#### 1.1.1.3 例四:字符串全排列问题 - -1、打印一个字符串的全部排列,process - -2、打印一个字符串的全部排列,要求不要出现重复的排列.process2。方法1,可以用HashSet最后去重,该方式是把递归的所有结果进行筛选。方法2可以抛弃重复元素,例如a在0位置已经尝试完毕,再有一个元素也是a要到0位置,那么禁止,该方法是递归的时候事先判断要不要进行下一步递归,更快一点。该方法又叫分支限界 - -```Java -package class11; - -import java.util.ArrayList; -import java.util.List; - -public class Code03_PrintAllPermutations { - - public static ArrayList permutation(String str) { - ArrayList res = new ArrayList<>(); - if (str == null || str.length() == 0) { - return res; - } - char[] chs = str.toCharArray(); - process(chs, 0, res); - return res; - } - - // str[0..i-1]已经做好决定的 - // str[i...]都有机会来到i位置 - // i到达终止位置,str当前的样子,就是一种结果 -> ans - public static void process(char[] str, int i, ArrayList ans) { - // i来到终点,返回该种答案 - if (i == str.length) { - ans.add(String.valueOf(str)); - } - // 如果i没有终止,i... 都可以来到i位置 - for (int j = i; j < str.length; j++) { // i后面所有的字符都有机会来到i位置 - swap(str, i, j); - process(str, i + 1, ans); - // 恢复交换之前的现场 - swap(str, i, j); - } - } - - - public static ArrayList permutationNoRepeat(String str) { - ArrayList res = new ArrayList<>(); - if (str == null || str.length() == 0) { - return res; - } - char[] chs = str.toCharArray(); - process2(chs, 0, res); - return res; - } - - // str[0..i-1]已经做好决定的 - // str[i...]都有机会来到i位置 - // i终止位置,str当前的样子,就是一种结果 -> ans - public static void process2(char[] str, int i, ArrayList res) { - if (i == str.length) { - res.add(String.valueOf(str)); - return; - } - boolean[] visit = new boolean[26]; // visit[0 1 .. 25] 代表a-z的字符有没有在当前出现过 - // i右边的字符都有机会 - for (int j = i; j < str.length; j++) { - // str[j] = 'a' -> 0 visit[0] -> 'a' - - // str[j] = 'z' -> 25 visit[25] -> 'z' - // 如果没出现过就没有机会 - if (!visit[str[j] - 'a']) { - - visit[str[j] - 'a'] = true; - swap(str, i, j); - process2(str, i + 1, res); - swap(str, i, j); - - } - } - } - - public static void swap(char[] chs, int i, int j) { - char tmp = chs[i]; - chs[i] = chs[j]; - chs[j] = tmp; - } - - public static void main(String[] args) { - String s = "aac"; - List ans1 = permutation(s); - for (String str : ans1) { - System.out.println(str); - } - System.out.println("======="); - List ans2 = permutationNoRepeat(s); - for (String str : ans2) { - System.out.println(str); - } - - } - -} - -``` - -#### 1.1.1.4 例六:用递归逆序一个栈(考验脑回路) - -给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现? - -> 思路,先不要想着逆序它,实现一个f函数,f函数的作用是把栈传进去,想办法拿到栈低元素并返回。 - -```Java -package class11; - -import java.util.Stack; - -public class Code04_ReverseStackUsingRecursive { - - public static void reverse(Stack stack) { - if (stack.isEmpty()) { - return; - } - // i是栈底元素,f调整之后的栈成为去掉栈底元素后的栈 - int i = f(stack); - // 递归调用 - reverse(stack); - // 经过base case之后,栈为空,此时压入的就是当前的栈底 - stack.push(i); - } - - public static int f(Stack stack) { - // 弹出栈顶,用result临时变量记住这个栈顶元素 - int result = stack.pop(); - // 如果栈为空,向上返回弹出的result,此时result就是栈底元素 - if (stack.isEmpty()) { - return result; - } else { - // 此时栈不为空,让子问题给我一个临时变量,临时变量就是子问题base case返回的result - int last = f(stack); - // 把我弹出的result再压入栈,向上返回子问题给我的result - stack.push(result); - return last; - } - } - - public static void main(String[] args) { - Stack test = new Stack(); - test.push(1); - test.push(2); - test.push(3); - test.push(4); - test.push(5); - reverse(test); - while (!test.isEmpty()) { - System.out.println(test.pop()); - } - - } - -} - -``` - -## 1.2 动态规划模型 - -> 以下代码有些已经实现了动态递归,看不懂没关系,下一章详细解释。可以先不管,回过头再看 - - -### 1.2.1 从左往右尝试模型 - -#### 1.2.1.1 数字字符转化问题 - -> 注:facebook面试题 - -1、规定1和A对应,2和B对应,3和C对应...。那么一个数字字符比如"111"就可以转化为:"AAA","KA","AK"。 - -给定一个只有数字字符组成的字符串str,返回有多少种转化结果 - -> 思路:根据从左往右,我们划分多大,来尝试,比如111,我们尝试一个1,为"A",剩下两个1去继续尝试。如果我们两个1尝试,就是"K"。三个1超过26字符,无法尝试。继续如此周而复始 - -```Java -package class11; - -public class Code06_ConvertToLetterString { - - public static int number(String str) { - if (str == null || str.length() == 0) { - return 0; - } - // i初始为0,表示0位置往后有多少中转化结果 - return process(str.toCharArray(), 0); - } - - // str[0...i-1]已经转化完了,固定了 - // i之前的位置,如何转化已经做过决定了, 不用再关心 - // i... 有多少种转化的结果 - public static int process(char[] str, int i) { - // i和字符串长度一样大,右侧没有字符了。找到1中转化是0~n-1位置的转化 - if (i == str.length) { // base case - return 1; - } - // i之前的决策,让当前i位置单独面对一个0字符,那么之前决策错误,返回0 - // 例如10先转化为A,2位置是0字符无法转化,当前决策无效 - // 而10直接转化为J,直接到终止位置,返回一种转化J - if (str[i] == '0') { - return 0; - } - - // i位置如果是1或者2,有可能和下一个位置共同转化,因为字符数为0~26 - // 反之3~9超过26不需要决策 - - // str[i] 如果是1,总是有两个选择,因为最大为19,不超过26 - if (str[i] == '1') { - int res = process(str, i + 1); - if (i + 1 < str.length) { - res += process(str, i + 2); - } - return res; - } - // str[i] 如果是2,那么有可能有两种选择,需要看是否朝贡国26 - if (str[i] == '2') { - int res = process(str, i + 1); - if (i + 1 < str.length && (str[i + 1] >= '0' && str[i + 1] <= '6')) { - res += process(str, i + 2); // (i和i+1)作为单独的部分,后续有多少种方法 - } - return res; - } - // str[i] 在3~9的位置,下个位置必须决策一种选择 - return process(str, i + 1); - } - - public static int dpWays2(String s) { - if (s == null || s.length() == 0) { - return 0; - } - char[] str = s.toCharArray(); - int N = str.length; - int[] dp = new int[N+1]; - dp[N] = 1; - for(int i = N-1; i >= 0; i--) { - if (str[i] == '0') { - dp[i] = 0; - } - if (str[i] == '1') { - dp[i] = dp[i + 1]; - if (i + 1 < str.length) { - dp[i] += dp[i + 2]; - } - } - if (str[i] == '2') { - dp[i] = dp[i + 1]; - if (i + 1 < str.length && (str[i + 1] >= '0' && str[i + 1] <= '6')) { - dp[i] += dp[i + 2]; // (i和i+1)作为单独的部分,后续有多少种方法 - } - } - } - return dp[0]; - } - - - public static int dpWays(String s) { - if (s == null || s.length() == 0) { - return 0; - } - char[] str = s.toCharArray(); - int N = str.length; - int[] dp = new int[N + 1]; - dp[N] = 1; - for (int i = N - 1; i >= 0; i--) { - if (str[i] == '0') { - dp[i] = 0; - } else if (str[i] == '1') { - dp[i] = dp[i + 1]; - if (i + 1 < N) { - dp[i] += dp[i + 2]; - } - } else if (str[i] == '2') { - dp[i] = dp[i + 1]; - if (i + 1 < str.length && (str[i + 1] >= '0' && str[i + 1] <= '6')) { - dp[i] += dp[i + 2]; - } - } else { - dp[i] = dp[i + 1]; - } - } - return dp[0]; - } - - public static void main(String[] args) { - System.out.println(number("11111")); - System.out.println(dpWays2("11111")); - } - -} - -``` - -#### 1.2.1.2 背包价值问题 - -2、给定两个长度都为N的数组weights和values,weight[i]和values[i]分别代表i号物品的重量和价值。 - -给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少? - -```Java -package class11; - -public class Code07_Knapsack { - - public static int getMaxValue(int[] w, int[] v, int bag) { - return process(w, v, 0, 0, bag); - } - - // 第一种尝试 - // 不变 : w[] 重量数组 v[] 价值数组 bag 袋子的总载重 - // index... 最大价值 - // 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); - - } - - public static int maxValue(int[] w, int[] v, int bag) { - return process(w, v, 0, bag); - } - - // 第二种尝试。更经典 - // 只剩下rest的空间了, - // index...货物自由选择,但是剩余空间不要小于0 - // 返回 index...货物能够获得的最大价值 - 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); - } - - public static int dpWay(int[] w, int[] v, int bag) { - int N = w.length; - int[][] dp = new int[N + 1][bag + 1]; - for (int index = N - 1; index >= 0; index--) { - for (int rest = 1; rest <= bag; rest++) { - dp[index][rest] = dp[index + 1][rest]; - if (rest >= w[index]) { - dp[index][rest] = Math.max(dp[index][rest], v[index] + dp[index + 1][rest - w[index]]); - } - } - } - return dp[0][bag]; - } - - public static void main(String[] args) { - int[] weights = { 3, 2, 4, 7 }; - int[] values = { 5, 6, 3, 19 }; - int bag = 11; - System.out.println(maxValue(weights, values, bag)); - System.out.println(dpWay(weights, values, bag)); - } - -} -``` - -### 1.2.2 范围上的尝试模型 - -#### 1.2.2.1 玩家抽取纸牌问题 - -给定一个整形数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B依次拿走每张纸牌。规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请赶回最后获胜者的分数 - -> 绝顶聪明学术上的解释:双方玩家都会使得对方玩家在当前单独改变策略时,不会获得更大的收益。 - -```Java -package class11; - -public class Code08_CardsInLine { - - // 主函数 - public static int win1(int[] arr) { - if (arr == null || arr.length == 0) { - return 0; - } - // 先手在0~length-1和后手在0~length-1上,谁分数大就是获胜者的分数 - return Math.max( - f(arr, 0, arr.length - 1), - s(arr, 0, arr.length - 1) - ); - } - - // L....R 先手函数 - // F S L+1..R - // L..R-1 - public static int f(int[] arr, int L, int R) { - // base case 当只剩一张牌,且是先手 - if (L == R) { - return arr[L]; - } - - // 当前是先手,选择最好的 - return Math.max( - arr[L] + s(arr, L + 1, R), - arr[R] + s(arr, L, R - 1) - ); - } - - // 后手函数 - // arr[L..R] - public static int s(int[] arr, int L, int R) { - // base case 当只剩一张牌,且为后手 - if (L == R) { - return 0; - } - - // 当前是后手,好的被绝顶聪明的先手选走了 - // 相当于是先手的决策剩下的当前牌,留下最差的min - return Math.min( - f(arr, L + 1, R), // 对手挑了 arr[i] - f(arr, L, R - 1) // 对手挑了 arr[j] - ); - } - - public static int win2(int[] arr) { - if (arr == null || arr.length == 0) { - return 0; - } - int N = arr.length; - int[][] f = new int[N][N]; - int[][] s = new int[N][N]; - for(int i = 0; i < N;i++) { - f[i][i] = arr[i]; - } - // s[i][i] = 0; - for(int i = 1; i < N;i++) { - int L =0; - int R =i; - while(L < N && R < N) { - - f[L][R] = Math.max( - arr[L] + s[L + 1][ R], - arr[R] + s[L][R - 1] - ); - s[L][R] = Math.min( - f[L + 1][R], // arr[i] - f[L][R - 1] // arr[j] - ); - - L++; - R++; - - } - } - return Math.max(f[0][N-1], s[0][N-1]); - } - - public static void main(String[] args) { - int[] arr = { 4,7,9,5,19,29,80,4 }; - // A 4 9 - // B 7 5 - System.out.println(win1(arr)); - System.out.println(win2(arr)); - - } - -} - -``` -#### 1.2.2.2 N皇后问题 - -N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行,不同列,也不在同一条斜线上。 - -给定一个整数n,返回n皇后的摆法有多少种。 - -n=1,返回1 - -n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0 - -n=8,返回92 -> 最优的N皇后的尝试方法,时间复杂度为N^N,但是process2是用位运算加速了常数项的时间 - -```Java -package class11; - -public class Code09_NQueens { - - public static int num1(int n) { - if (n < 1) { - return 0; - } - // record[0] ? record[1] ? record[2] - // record[i] -> i行的皇后,放在了第几列 - int[] record = new int[n]; - return process1(0, record, n); - } - - // 潜台词:record[0..i-1]的皇后,任何两个皇后一定都不共行、不共列,不共斜线 - // 目前来到了第i行,在i行准备放皇后 - // record[0..i-1]表示之前的行,放了的皇后位置 - // n代表整体一共有多少行 0~n-1行 - // 返回值是,摆完所有的皇后,合理的摆法有多少种 - // 尝试过程 - public static int process1(int i, int[] record, int n) { - if (i == n) { // 终止行 - return 1; - } - // 没有到终止位置,还有皇后要摆 - int res = 0; - // 当前行在i行,尝试i行所有的列 -> j - for (int j = 0; j < n; j++) { - // 当前i行的皇后,放在j列,会不会和之前(0..i-1)的皇后,不共行共列共斜线, - // 如果是,认为有效,当前可以摆在j列的位置 - // 如果不是,认为无效 - if (isValid(record, i, j)) { - // 当前存在i行的有效值为j列位置 - record[i] = j; - res += process1(i + 1, record, n); - } - } - return res; - } - - // record[0..i-1]你需要看,record[i...]不需要看 - // 返回i行皇后,放在了j列,是否有效 - // a行b列的皇后,和c行d列的皇后会不会冲突,coding的条件是不共行 - // 共列的话b==d,共斜线的话|a-c|==|b-d| - public static boolean isValid(int[] record, int i, int j) { - // 之前的某个k行的皇后 - for (int k = 0; k < i; k++) { - // k, record[k] i, j - if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) { - return false; - } - } - return true; - } - - // 请不要超过32皇后问题 - public static int num2(int n) { - if (n < 1 || n > 32) { - return 0; - } - // 如果你是13皇后问题,limit 最右13个1,其他都是0 - int limit = n == 32 ? -1 : (1 << n) - 1; - return process2(limit, 0, 0, 0); - } - - // limit 划定了问题的规模 -> 固定 - - // 用位运算加速常数项时间 - // colLim 列的限制,1的位置不能放皇后,0的位置可以 - // leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以 - // rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以 - public static int process2( - int limit, - int colLim, - int leftDiaLim, - int rightDiaLim) { - if (colLim == limit) { // base case - return 1; - } - // 所有可以放皇后的位置,都在pos上 - // colLim | leftDiaLim | rightDiaLim -> 总限制 - // ~ (colLim | leftDiaLim | rightDiaLim) -> 左侧的一坨0干扰,右侧每个1,可尝试 - - // 把左侧的去反后的一坨1,移除掉 - int pos = limit & ( ~(colLim | leftDiaLim | rightDiaLim) ); - int mostRightOne = 0; - int res = 0; - while (pos != 0) { - // 提取出pos中,最右侧的1来,剩下位置都是0 - mostRightOne = pos & (~pos + 1); - pos = pos - mostRightOne; - res += process2(limit, - colLim | mostRightOne, - (leftDiaLim | mostRightOne) << 1, - (rightDiaLim | mostRightOne) >>> 1); - } - return res; - } - - public static void main(String[] args) { - int n = 15; - - long start = System.currentTimeMillis(); - System.out.println(num2(n)); - long end = System.currentTimeMillis(); - System.out.println("cost time: " + (end - start) + "ms"); - - start = System.currentTimeMillis(); - System.out.println(num1(n)); - end = System.currentTimeMillis(); - System.out.println("cost time: " + (end - start) + "ms"); - - } -} -``` - -## 如何尝试一件事? - -1、有经验但是没有方法论 - -2、怎么判断一个尝试 - -3、难道尝试这件事真的只能拼天赋么,该怎么搞定面试? - -4、动态规划是啥?好高端的样子,和尝试有什么关系? - -请见下一章,暴力递归到动态规划的转移套路,解决面试中动态规划的问题。 diff --git "a/12-\346\232\264\345\212\233\351\200\222\345\275\222\345\210\260\345\212\250\346\200\201\350\247\204\345\210\222.md" "b/12-\346\232\264\345\212\233\351\200\222\345\275\222\345\210\260\345\212\250\346\200\201\350\247\204\345\210\222.md" deleted file mode 100644 index e19a96d..0000000 --- "a/12-\346\232\264\345\212\233\351\200\222\345\275\222\345\210\260\345\212\250\346\200\201\350\247\204\345\210\222.md" +++ /dev/null @@ -1,1148 +0,0 @@ -[TOC] -# 1 暴力递归到动态规划 - -==动态规划最核心的是暴力递归的尝试过程,一旦通过尝试写出来暴力递归的解法,那么动态规划在此基础上就很好改了== - -> 1、暴力递归之所以暴力,是因为存在大量的重复计算。加入我们定义我们的缓存结构,用来查该状态有没有计算过,那么会加速我们的递归 - -> 2、在我们加入缓存结构之后,消除了大量的重复计算,缓存表就是我们的dp表。那么这种去除重复计算的递归,就是最粗糙的动态规划,也叫记忆化搜索 - -> 3、如果我们把我们的dp表,从简单到复杂列出来,那么就是经典的动态规划。我们无需考虑转移方程怎么写,而是根据我们的递归来推导。看下面例子: - -## 1.1 例一 : 机器人运动问题(2018阿里面试题目) - -> 认识暴力递归改动态规划的过程 - - -假设有排成一行的N个位置,记为1~N,N一定大于等于2。开始时机器人在其中的M位置上(M一定是1~N中的一个)。 - -如果机器人来到1位置,那么下一步只能往右来到2位置; - -如果机器人来到N位置,那么下一步只能往左来到N-1的位置; - -如果机器人来到中间位置,那么下一步可以往左走或者往右走; - -规定机器人必须走K步,最终能来到P位置(P也是1~N中的一个)的方法有多少种? - -给定四个参数N,M,K,P。返回方法数 - -> 暴力递归ways1调用的walk函数,就是暴力递归过程,存在重复计算。waysCache方法对应的walkCache就是在纯暴力递归的基础上加了缓存 - -==假设我们的参数N=7,M=2,K=5,P=3。我们根据我们的递归加缓存的过程来填我们的dp表:== - -1、int[][] dp = new int[N+1][K+1];是我们的DP表的范围 - -2、当rest==0的时候,dp[cur][rest] = cur == P ? 1 : 0; 我们只有cur==P的时候为1,其他位置都为0。 - -3、当cur==1的时候,dp[cur][rest] = walkCache(N, 2, rest - 1, P, dp); 我们的dp当前的值,依赖于cur的下一个位置,rest的上一个位置 - -4、当cur == N的时候,dp[cur][rest] =walkCache(N, N - 1, rest - 1, P,dp); 我们dp当前位置依赖于N-1位置,和rest - 1位置 - -5、当cur在任意中间位置时。dp[cur][rest] = walkCache(N, cur + 1, rest - 1, P,dp) + walkCache(N, cur - 1, rest - 1, P, dp); dp的当前位置依赖于dp的cur+1和rest-1位置加上dp的cur-1和rest-1的位置 - -那么我们可以得到我们的DP表为: - -```text - - 0 1 2 3 4 5 K 坐标 -0 x x x x x x - -1 0 0 1 0 3 0 - -2 0 1 0 3 0 9 - -3 1 0 2 0 6 0 - -4 0 1 0 3 0 10 - -5 0 0 1 0 5 0 - -6 0 0 0 1 0 5 - -7 0 0 0 0 1 0 - -cur -坐 -标 -``` - -==所以任何的动态规划,都可以由暴力递归改出来。也就是所任意的动态规划都来自于某个暴力递归。反之任何暴力递归不一定能改成动态规划,加入某暴力递归并没有重复计算,没有缓存的必要== - -==动态规划实质就是把参数组合完成结构化的缓存== - -```Java -package class12; - -public class Code01_RobotWalk { - - public static int ways1(int N, int M, int K, int P) { - // 参数无效直接返回0 - if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) { - return 0; - } - // 总共N个位置,从M点出发,还剩K步可以走,返回最终能达到P的方法数 - return walk(N, M, K, P); - } - - // N : 位置为1 ~ N,固定参数 - // cur : 当前在cur位置,可变参数 - // rest : 还剩res步没有走,可变参数 - // P : 最终目标位置是P,固定参数 - // 该函数的含义:只能在1~N这些位置上移动,当前在cur位置,走完rest步之后,停在P位置的方法数作为返回值返回 - public static int walk(int N, int cur, int rest, int P) { - // 如果没有剩余步数了,当前的cur位置就是最后的位置 - // 如果最后的位置停在P上,那么之前做的移动是有效的 - // 如果最后的位置没在P上,那么之前做的移动是无效的 - if (rest == 0) { - return cur == P ? 1 : 0; - } - // 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2 - // 后续的过程就是,来到2位置上,还剩rest-1步要走 - if (cur == 1) { - return walk(N, 2, rest - 1, P); - } - // 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1 - // 后续的过程就是,来到N-1位置上,还剩rest-1步要走 - if (cur == N) { - return walk(N, N - 1, rest - 1, P); - } - // 如果还有rest步要走,而当前的cur位置在中间位置上,那么当前这步可以走向左,也可以走向右 - // 走向左之后,后续的过程就是,来到cur-1位置上,还剩rest-1步要走 - // 走向右之后,后续的过程就是,来到cur+1位置上,还剩rest-1步要走 - // 走向左、走向右是截然不同的方法,所以总方法数要都算上 - return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P); - } - - public static int waysCache(int N, int M, int K, int P) { - // 参数无效直接返回0 - if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) { - return 0; - } - - // 我们准备一张缓存的dp表 - // 由于我们的cur范围是1~N,我们准备N+1。 - // rest范围在1~K。我们准备K+1。 - // 目的是把我们的可能结果都能装得下 - int[][] dp = new int[N+1][K+1]; - // 设置这张表的初始值都为-1,代表都还没用过 - for(int row = 0; row <= N; row++) { - for(int col = 0; col <= K; col++) { - dp[row][col] = -1; - } - } - return walkCache(N, M, K, P,dp); - } - - // HashMap (19,100) "19_100" - // 我想把所有cur和rest的组合,返回的结果,加入到缓存里 - public static int walkCache(int N, int cur, int rest, int P, int[][] dp) { - // 当前场景已经计算过,不要再暴力展开,直接从缓存中拿 - if(dp[cur][rest] != -1) { - return dp[cur][rest]; - } - if (rest == 0) { - // 先加缓存 - dp[cur][rest] = cur == P ? 1 : 0; - return dp[cur][rest]; - } - if (cur == 1) { - // 先加缓存 - dp[cur][rest] = walkCache(N, 2, rest - 1, P, dp); - return dp[cur][rest]; - } - if (cur == N) { - // 先加缓存 - dp[cur][rest] =walkCache(N, N - 1, rest - 1, P,dp); - return dp[cur][rest]; - } - // 先加缓存 - dp[cur][rest] = walkCache(N, cur + 1, rest - 1, P,dp) - + walkCache(N, cur - 1, rest - 1, P, dp); - return dp[cur][rest]; - } - - public static int ways2(int N, int M, int K, int P) { - // 参数无效直接返回0 - if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) { - return 0; - } - int[][] dp = new int[K + 1][N + 1]; - dp[0][P] = 1; - for (int i = 1; i <= K; i++) { - for (int j = 1; j <= N; j++) { - if (j == 1) { - dp[i][j] = dp[i - 1][2]; - } else if (j == N) { - dp[i][j] = dp[i - 1][N - 1]; - } else { - dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1]; - } - } - } - return dp[K][M]; - } - - public static int ways3(int N, int M, int K, int P) { - // 参数无效直接返回0 - if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) { - return 0; - } - int[] dp = new int[N + 1]; - dp[P] = 1; - for (int i = 1; i <= K; i++) { - int leftUp = dp[1];// 左上角的值 - for (int j = 1; j <= N; j++) { - int tmp = dp[j]; - if (j == 1) { - dp[j] = dp[j + 1]; - } else if (j == N) { - dp[j] = leftUp; - } else { - dp[j] = leftUp + dp[j + 1]; - } - leftUp = tmp; - } - } - return dp[M]; - } - - // ways4是你的方法 - public static int ways4(int N, int M, int K, int P) { - if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) { - return 0; - } - return process(N, 0, P, M, K); - } - - // 一共N个位置,从M点出发,一共只有K步。返回走到位置j,剩余步数为i的方法数 - public static int process(int N, int i, int j, int M, int K) { - if (i == K) { - return j == M ? 1 : 0; - } - if (j == 1) { - return process(N, i + 1, j + 1, M, K); - } - if (j == N) { - return process(N, i + 1, j - 1, M, K); - } - return process(N, i + 1, j + 1, M, K) + process(N, i + 1, j - 1, M, K); - } - - // ways5是你的方法的dp优化 - public static int ways5(int N, int M, int K, int P) { - if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) { - return 0; - } - int[][] dp = new int[K + 1][N + 1]; - dp[K][M] = 1; - for (int i = K - 1; i >= 0; i--) { - for (int j = 1; j <= N; j++) { - if (j == 1) { - dp[i][j] = dp[i + 1][j + 1]; - } else if (j == N) { - dp[i][j] = dp[i + 1][j - 1]; - } else { - dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j - 1]; - } - } - } - return dp[0][P]; - } - - public static void main(String[] args) { - System.out.println(ways1(7, 4, 9, 5)); - System.out.println(ways2(7, 4, 9, 5)); - System.out.println(ways3(7, 4, 9, 5)); - System.out.println(ways4(7, 4, 9, 5)); - System.out.println(ways5(7, 4, 9, 5)); - } - -} - -``` - -## 1.2 例二:背包问题改动态规划 - -> 暴力递归过程各种,我们如何组织我们的决策过程。实质就是动态规划的状态转移 - -```Java -package class12; - -public class Code03_Knapsack { - - public static int getMaxValue(int[] w, int[] v, int bag) { - // 主函数需要0 bag的返回值 - return process(w, v, 0, 0, bag); - } - - // index... 最大价值 - public static int process(int[] w, int[] v, int index, int alreadyW, int bag) { - if (alreadyW > bag) { - return -1; - } - // 重量没超 - if (index == w.length) { - return 0; - } - int p1 = process(w, v, index + 1, alreadyW, bag); - int p2next = process(w, v, index + 1, alreadyW + w[index], bag); - int p2 = -1; - if (p2next != -1) { - p2 = v[index] + p2next; - } - return Math.max(p1, p2); - - } - - public static int maxValue(int[] w, int[] v, int bag) { - return process(w, v, 0, bag); - } - - // 只剩下rest的空间了, - // index...货物自由选择,但是不要超过rest的空间 - // 返回能够获得的最大价值 - public static int process(int[] w, int[] v, int index, int rest) { - if (rest < 0) { // base case 1 - return -1; - } - // rest >=0 - if (index == w.length) { // base case 2 - return 0; - } - // 有货也有空间 - int p1 = process(w, v, index + 1, rest); - int p2 = -1; - int p2Next = process(w, v, index + 1, rest - w[index]); - if(p2Next!=-1) { - p2 = v[index] + p2Next; - } - return Math.max(p1, p2); - } - - // dpWay就是把上述的暴力递归改为动态规划 - public static int dpWay(int[] w, int[] v, int bag) { - int N = w.length; - // 准备一张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表格,注释位置是正常递归处理 - - // int p1 = process(w, v, index + 1, rest); - // int p2 = -1; - // int p2Next = process(w, v, index + 1, rest - w[index]); - // if(p2Next!=-1) { - // p2 = v[index] + p2Next; - // } - // return Math.max(p1, p2); - - // 所以我们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]; - } - - public static void main(String[] args) { - int[] weights = { 3, 2, 4, 7 }; - int[] values = { 5, 6, 3, 19 }; - int bag = 11; - System.out.println(maxValue(weights, values, bag)); - System.out.println(dpWay(weights, values, bag)); - } - -} -``` - -## 1.3 动态规划解题思路 - -1、拿到题目先找到某一个暴力递归的写法(尝试) - -2、分析我们的暴力递归,是否存在重复解。没有重复解的递归没必要改动态规划 - -3、把暴力递归中的可变参数,做成缓存结构,不讲究组织。即没算过加入缓存结构,算过的直接拿缓存,就是记忆化搜索 - -4、如果把缓存结构做精细化组织,就是我们经典的动态规划 - -5、以背包问题举例,我们每一个重量有要和不要两个选择,且都要递归展开。那么我们的递归时间复杂度尾O(2^N)。而记忆化搜索,根据可变参数得到的长为N价值为W的二维表,那么我们的时间复杂度为O(N*bag)。如果递归过程中状态转移有化简继续优化的可能,例如枚举。那么经典动态规划可以继续优化,否则记忆化搜索和动态规划的时间复杂度是一样的 - -### 1.3.1 凑货币问题(重要) - -> 该题是对动态规划完整优化路径的演示 - -有一个表示货币面值的数组arr,每种面值的货币可以使用任意多张。arr数组元素为正数,且无重复值。例如[7,3,100,50]这是四种面值的货币。 - -问:给定一个特定金额Sum,我们用货币面值数组有多少种方法,可以凑出该面值。例如P=1000,用这是四种面值有多少种可能凑出1000 - -> ways1为暴力递归的解题思路及实现 - -> ways2为暴力递归改记忆化搜索的版本 - -> ways3为记忆化搜索版本改动态规划的版本 - -```Java -package class12; - -public class Code09_CoinsWay { - - // arr中都是正数且无重复值,返回组成aim的方法数,暴力递归 - public static int ways1(int[] arr, int aim) { - if (arr == null || arr.length == 0 || aim < 0) { - return 0; - } - return process1(arr, 0, aim); - } - - public static int process1(int[] arr, int index, int rest) { - // base case - // 当在面值数组的arr.length,此时越界,没有货币可以选择。 - // 如果当前目标金额就是0,那么存在一种方法,如果目标金额不为0,返回0中方法 - if(index == arr.length) { - return rest == 0 ? 1 : 0 ; - } - - // 普遍位置 - int ways = 0; - // 从0号位置开始枚举,选择0张,1张,2张等 - // 条件是张数乘以选择的面值,不超过木匾面值rest - for(int zhang = 0; zhang * arr[index] <= rest ;zhang++) { - // 方法数加上除去当前选择后所剩面额到下一位置的选择数,递归 - ways += process1(arr, index + 1, rest - (zhang * arr[index]) ); - } - return ways; - } - - - // ways1暴力递归,改为记忆化搜索。ways2为记忆化搜索版本 - public static int ways2(int[] arr, int aim) { - if (arr == null || arr.length == 0 || aim < 0) { - return 0; - } - // 缓存结构,且只和index和rest有关,跟arr无关 - int[][] dp = new int[arr.length+1][aim+1]; - // 一开始所有的过程,都没有计算呢,dp二维表初始化为-1 - // dp[..][..] = -1 - for(int i = 0 ; i < dp.length; i++) { - for(int j = 0 ; j < dp[0].length; j++) { - dp[i][j] = -1; - } - } - // 缓存结构向下传递 - return process2(arr, 0, aim , dp); - } - - // 如果index和rest的参数组合,是没算过的,dp[index][rest] == -1 - // 如果index和rest的参数组合,是算过的,dp[index][rest] > - 1 - public static int process2(int[] arr, - int index, int rest, - int[][] dp) { - if(dp[index][rest] != -1) { - return dp[index][rest]; - } - if(index == arr.length) { - dp[index][rest] = rest == 0 ? 1 :0; - return dp[index][rest]; - } - int ways = 0; - for(int zhang = 0; zhang * arr[index] <= rest ;zhang++) { - // 返回之前加入缓存 - ways += process2(arr, index + 1, rest - (zhang * arr[index]) , dp ); - } - // 返回之前加入缓存 - dp[index][rest] = ways; - return ways; - } - - // 记忆化搜索改造为动态规划版本,ways3。 - // 如果没有枚举行为,该动态该规划为自顶向下的动态规划和记忆化搜索等效,但这题存在枚举行为。 - public static int ways3(int[] arr, int aim) { - if (arr == null || arr.length == 0 || aim < 0) { - return 0; - } - int N = arr.length; - // dp表 - int[][] dp = new int[N + 1][aim + 1]; - // 根据递归方法,N为arr的越界位置,但是我们的dp表定义的是N+1。 - // N位置,如果aim为0,则dp[N][0] = 1; - dp[N][0] = 1;// dp[N][1...aim] = 0; - - // 每个位置依赖自己下面的位置,那么我们从下往上循环 - for(int index = N - 1; index >= 0; index--) { - // rest从左往右 - for(int rest = 0; rest <= aim; rest++) { - int ways = 0; - for(int zhang = 0; zhang * arr[index] <= rest ;zhang++) { - ways += dp[index + 1] [rest - (zhang * arr[index])]; - } - dp[index][rest] = ways; - } - } - // 最终我们需要[0,aim]位置的解 - return dp[0][aim]; - } - - // 由于存在枚举行为,我们可以进一步优化我们的动态规划。ways4是优化的动态规划 - // 根据枚举,用具体化例子来找出规律,省掉枚举 - public static int ways4(int[] arr, int aim) { - if (arr == null || arr.length == 0 || aim < 0) { - return 0; - } - int N = arr.length; - int[][] dp = new int[N + 1][aim + 1]; - dp[N][0] = 1;// dp[N][1...aim] = 0; - for(int index = N - 1; index >= 0; index--) { - for(int rest = 0; rest <= aim; rest++) { - dp[index][rest] = dp[index+1][rest]; - if(rest - arr[index] >= 0) { - dp[index][rest] += dp[index][rest - arr[index]]; - } - } - } - return dp[0][aim]; - } - - - public static void main(String[] args) { - int[] arr = { 5, 10,50,100 }; - int sum = 1000; - System.out.println(ways1(arr, sum)); - System.out.println(ways2(arr, sum)); - System.out.println(ways3(arr, sum)); - System.out.println(ways4(arr, sum)); - } - -} -``` - -### 1.3.2 贴纸问题 - -给定一个字符串str,给定一个字符串类型的数组arr。arr里的每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来。 - -返回需要至少多少张贴纸可以完成这个任务。 - -例如:str="babac",arr={"ba","c","abcd"} - -至少需要两张贴纸"ba"和"abcd",因为使用这两张贴纸,把每一个字符串单独剪开,含有2个a,2个b,1个c。是可以拼出str的,所以返回2。 - -> 思路1 minStickers1:由于任何贴纸都可以剪切的很碎,跟贴纸的顺序没关系。那么目标str我们先进行排序,那么也不会有影响。babac排序为aabbc,我们再去选择贴纸,str被贴纸贴好后,剩下的接着排序,再选择...... - - -> 由于我们的可变参数,只有一个目标字符串。但是目标字符串的可能性太多,没办法精细化动态规划。傻缓存的暴力递归已经是最优解了。所以只有一个可变参数,又存在重复解,那么傻缓存就是最优解。 - -> 思路2 minStickers2,我们每一张贴纸枚举所有可能的张数,后续过程不再考虑这张贴纸。但是方法一会好一点,因为只有一个可变参数。而方法二有两个可变参数,我们在设计递归的时候,尽量少的使用可变参数,这样我们缓存结构的命中率会提升 - -```Java -package class12; - -import java.util.Arrays; -import java.util.HashMap; - -public class Code02_StickersToSpellWord { - - // 暴力递归加缓存dp - public static int minStickers1(String[] stickers, String target) { - - int n = stickers.length; - // stickers -> [26] [26] [26] - // 表示把每张贴纸转化为26个字母的词频数组 - int[][] map = new int[n][26]; - // 把每一张贴纸转化的字母26个字母有多少个 - for (int i = 0; i < n; i++) { - char[] str = stickers[i].toCharArray(); - for (char c : str) { - map[i][c - 'a']++; - } - } - - HashMap dp = new HashMap<>(); - dp.put("", 0); - return process1(dp, map, target); - } - - // dp 傻缓存,如果rest已经算过了,直接返回dp中的值 - // rest 剩余的目标 - // 0..N每一个字符串所含字符的词频统计 - // 返回值是-1,map 中的贴纸 怎么都无法rest - public static int process1( - HashMap dp, - int[][] map, - String rest) { - if (dp.containsKey(rest)) { - return dp.get(rest); - } - // 以下就是正式的递归调用过程 - int ans = Integer.MAX_VALUE; // ans -> 搞定rest,使用的最少的贴纸数量 - int n = map.length; // N种贴纸 - int[] tmap = new int[26]; // tmap 去替代 rest - // 把目标target剩余的rest进行词频统计 - char[] target = rest.toCharArray(); - for (char c : target) { - tmap[c - 'a']++; - } - - // map表示所有贴纸的词频信息,tmap表示目标字符串的词频信息 - // 用map去覆盖tmap - for (int i = 0; i < n; i++) { - // 枚举当前第一张贴纸是谁? - // 第一张贴纸必须包含当前目标字符串的首字母。和所有贴纸去选等效 - // 目的是让当前贴纸的元素有在目标字符串的对应个,否则试过之后仍然是原始串,栈会溢出 - if (map[i][target[0] - 'a'] == 0) { - continue; - } - StringBuilder sb = new StringBuilder(); - // i 贴纸, j 枚举a~z字符 - for (int j = 0; j < 26; j++) { // - if (tmap[j] > 0) { // j这个字符是target需要的 - for (int k = 0; k < Math.max(0, tmap[j] - map[i][j]); k++) { - // 添加剩余 - sb.append((char) ('a' + j)); - } - } - } - // sb -> i 此时s就是当前贴纸搞定之后剩余的 - String s = sb.toString(); - // tmp是后续方案需要的贴纸数 - int tmp = process1(dp, map, s); - // 如果-1表示贴纸怎么都无法覆盖目标字符串 - if (tmp != -1) { - // 不是-1,最终返回当前后续贴纸数,加上当前贴纸数与ans的最小值 - ans = Math.min(ans, 1 + tmp); - } - } - // 经过上述循环,ans仍然系统最大,表示怎么都无法覆盖rest,返回-1 - dp.put(rest, ans == Integer.MAX_VALUE ? -1 : ans); - return dp.get(rest); - } - - - - public static int minStickers2(String[] stickers, String target) { - int n = stickers.length; - int[][] map = new int[n][26]; - for (int i = 0; i < n; i++) { - char[] str = stickers[i].toCharArray(); - for (char c : str) { - map[i][c - 'a']++; - } - } - char[] str = target.toCharArray(); - int[] tmap = new int[26]; - for (char c : str) { - tmap[c - 'a']++; - } - HashMap dp = new HashMap<>(); - int ans = process2(map, 0, tmap, dp); - return ans; - } - - public static int process2(int[][] map, int i, int[] tmap, HashMap dp) { - StringBuilder keyBuilder = new StringBuilder(); - keyBuilder.append(i + "_"); - for (int asc = 0; asc < 26; asc++) { - if (tmap[asc] != 0) { - keyBuilder.append((char) (asc + 'a') + "_" + tmap[asc] + "_"); - } - } - String key = keyBuilder.toString(); - if (dp.containsKey(key)) { - return dp.get(key); - } - boolean finish = true; - for (int asc = 0; asc < 26; asc++) { - if (tmap[asc] != 0) { - finish = false; - break; - } - } - if (finish) { - dp.put(key, 0); - return 0; - } - if (i == map.length) { - dp.put(key, -1); - return -1; - } - int maxZhang = 0; - for (int asc = 0; asc < 26; asc++) { - if (map[i][asc] != 0 && tmap[asc] != 0) { - maxZhang = Math.max(maxZhang, (tmap[asc] / map[i][asc]) + (tmap[asc] % map[i][asc] == 0 ? 0 : 1)); - } - } - int[] backup = Arrays.copyOf(tmap, tmap.length); - int min = Integer.MAX_VALUE; - int next = process2(map, i + 1, tmap, dp); - tmap = Arrays.copyOf(backup, backup.length); - if (next != -1) { - min = next; - } - for (int zhang = 1; zhang <= maxZhang; zhang++) { - for (int asc = 0; asc < 26; asc++) { - tmap[asc] = Math.max(0, tmap[asc] - (map[i][asc] * zhang)); - } - next = process2(map, i + 1, tmap, dp); - tmap = Arrays.copyOf(backup, backup.length); - if (next != -1) { - min = Math.min(min, zhang + next); - } - } - int ans = min == Integer.MAX_VALUE ? -1 : min; - dp.put(key, ans); - return ans; - } - - public static void main(String[] args) { - String[] arr = {"aaaa","bbaa","ccddd"}; - String str = "abcccccdddddbbbaaaaa"; - System.out.println(minStickers1(arr, str)); - System.out.println(minStickers2(arr, str)); - - - } - -} -``` - -## 1.4 什么暴力递归可以继续优化? - -1、有重复调用同一个子问题的解,这种递归可以优化 - -2、如果每一个子问题都是不同的解,无法优化也不用优化 - -## 1.5 暴力地柜和动态规划的关系 - -1、某个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划 - -2、任何动态规划问题,都一定对应着某一个有解的重复调用的暴力递归 - -3、不是所有的暴力递归,都一定对应着动态规划 - -## 1.6 面试题中和动态规划的关系 - -1、解决一个问题,可能有很多尝试方法 - -2、可能在很多尝试方法中,又有若干个尝试方法有动态规划的方式 - -3、一个问题,可能有若干种动态规划的解法 - -## 1.7 如何找到某个问题的动态规划方式? - -1、设计暴力递归:重要原则+4中常见尝试模型!重点 - -2、分析有没有重复解:套路解决 - -3、用记忆化搜索 -> 用严格表结构实现动态规划:套路解决 - -4、看看能否继续优化:套路解决 - -## 1.8 面试中设计暴力递归过程的原则 - -==怎么猜是不对的?== - -1、每一个可变参数,一定不要比int类型更加复杂 - -> 比如可变参数是int a和int b 那么我们的缓存结构可以是a*b的二维数组。大小取决于a和b的范围 - -> 但是如果我们的可变参数是一个数组,int[] a。那么如果过多,我们不太容易设计那么大的缓存结构。如果只有一个这种可变参数就是原则2 - -==不管什么问题,我们在脑海中想象可变参数就不会突破整形范围== - -2、原则1可以违反,让类型突破到一维线性结构,那必须是唯一可变参数。例如上述贴纸问题 - -3、如果发现原则1被违反,但没违反原则2。只需要做到记忆化搜索就是最优解 - -3、可变参数个数,能少则少 - - -## 1.9 笔试面试过程中怎么设计暴力递归? - -1、一定逼自己找到不违反1.8原则情况下的暴力尝试! - -2、如果你找到暴力尝试,不符合原则,马上舍弃,找新的! - -3、如果某个题目突破了设计原则,一定极难极难,面试中出现的概率低于5% - -## 1.10 常见的4种尝试模型 - -> 每一个模型背后都有很多题目 - -### 1.10.1 从左往右的尝试模型 - -> 例如背包问题 - -### 1.10.2 范围上的尝试模型 - -> 例如纸牌博弈问题 - -### 1.10.3 多样本位置全对应的尝试模型 - -#### 1.10.3.1 两个字符串的最长公共子序列问题 - -> 例如“ab1cd2ef345gh”和“opq123rs4tx5yz”的最长公共子序列为“12345”。即在两个字符串所有相等的子序列里最长的。所以返回子序列的长度5 - -> 假如"a123bc"和"12dea3fz"两个字符串,我们把这两个样本的下标一个作为行,一个作为列。观察这样的结构所表示的意义。dp表这样设计就是str1从0到i位置,str2从0到j位置,两个位置的最长公共子序列。 - -> dp[i][j] 表示的含义是,str1字符串在i位置, -str2在j位置,两个最长公共子序列多长。 -那么str1和str2的最长公共子序列,就是dp[str1.length][str2.length] - -> 对于任何位置dp[i][j]: - -> 1. 如果str1的i位置和str2的j位置的最长公共子序列跟str1的i位置字符和str2的j位置字符无关,那么此时的最长公共子序列长度就是dp[i-1][j-1] - -> 2. 此时与str1的i位置结尾的字符有关系,和str2的j位置结尾的字符没关系。此时跟str2的j位置没关系,最长公共子序列式dp[i][j-1] - -> 3. 此时与str1的i位置结尾的字符没关系,和str2的j位置结尾的字符有关系。此时跟str1的j位置没关系,最长公共子序列式dp[i-1][j] - -> 4. 此时即与str1的i字符结尾,有关系。又与str2的j位置结尾,有关系。只有str1[i]==str2[j]才有可能存在这种情况,且为dp[i-1][j-1] + 1 - - -```text - - 0 1 2 3 4 5 6 7 str2 -0 0 0 0 0 1 1 1 1 - -1 0 - -2 1 - -3 1 - -4 1 - -5 1 - -str1 - -``` - -```Java -package class12; - -public class Code05_PalindromeSubsequence { - - 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]; - } - - public static void main(String[] args) { - - } - -} - -``` - -### 1.10.4 寻找业务限制的尝试模型 - -> 四种模型中最难的一种,暂时只看process方法,process更改为动态规划的dp方法。后面会展开说明其他方法, - -例题:给定一个数组,代表每个人喝完咖啡准备刷杯子的时间,只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯。每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发。返回让所有咖啡杯变干净的最早完成时间。三个参数:int[]arr , int a , int b(京东) - - -```Java -package class12; - -import java.util.Arrays; -import java.util.Comparator; -import java.util.PriorityQueue; - -// 题目 -// 数组arr代表每一个咖啡机冲一杯咖啡的时间,每个咖啡机只能串行的制造咖啡。 -// 现在有n个人需要喝咖啡,只能用咖啡机来制造咖啡。 -// 认为每个人喝咖啡的时间非常短,冲好的时间即是喝完的时间。 -// 每个人喝完之后咖啡杯可以选择洗或者自然挥发干净,只有一台洗咖啡杯的机器,只能串行的洗咖啡杯。 -// 洗杯子的机器洗完一个杯子时间为a,任何一个杯子自然挥发干净的时间为b。 -// 四个参数:arr, n, a, b -// 假设时间点从0开始,返回所有人喝完咖啡并洗完咖啡杯的全部过程结束后,至少来到什么时间点。 -public class Code06_Coffee { - - // 方法一:暴力尝试方法 - public static int minTime1(int[] arr, int n, int a, int b) { - int[] times = new int[arr.length]; - int[] drink = new int[n]; - return forceMake(arr, times, 0, drink, n, a, b); - } - - // 方法一,每个人暴力尝试用每一个咖啡机给自己做咖啡 - public static int forceMake(int[] arr, int[] times, int kth, int[] drink, int n, int a, int b) { - if (kth == n) { - int[] drinkSorted = Arrays.copyOf(drink, kth); - Arrays.sort(drinkSorted); - return forceWash(drinkSorted, a, b, 0, 0, 0); - } - int time = Integer.MAX_VALUE; - for (int i = 0; i < arr.length; i++) { - int work = arr[i]; - int pre = times[i]; - drink[kth] = pre + work; - times[i] = pre + work; - time = Math.min(time, forceMake(arr, times, kth + 1, drink, n, a, b)); - drink[kth] = 0; - times[i] = pre; - } - return time; - } - - // 方法一,暴力尝试洗咖啡杯的方式 - public static int forceWash(int[] drinks, int a, int b, int index, int washLine, int time) { - if (index == drinks.length) { - return time; - } - // 选择一:当前index号咖啡杯,选择用洗咖啡机刷干净 - int wash = Math.max(drinks[index], washLine) + a; - int ans1 = forceWash(drinks, a, b, index + 1, wash, Math.max(wash, time)); - - // 选择二:当前index号咖啡杯,选择自然挥发 - int dry = drinks[index] + b; - int ans2 = forceWash(drinks, a, b, index + 1, washLine, Math.max(dry, time)); - return Math.min(ans1, ans2); - } - - // 方法二:稍微好一点的解法 - public static class Machine { - public int timePoint; - public int workTime; - - public Machine(int t, int w) { - timePoint = t; - workTime = w; - } - } - - public static class MachineComparator implements Comparator { - - @Override - public int compare(Machine o1, Machine o2) { - return (o1.timePoint + o1.workTime) - (o2.timePoint + o2.workTime); - } - - } - - // 方法二,每个人暴力尝试用每一个咖啡机给自己做咖啡,优化成贪心 - public static int minTime2(int[] arr, int n, int a, int b) { - PriorityQueue heap = new PriorityQueue(new MachineComparator()); - for (int i = 0; i < arr.length; i++) { - heap.add(new Machine(0, arr[i])); - } - int[] drinks = new int[n]; - for (int i = 0; i < n; i++) { - Machine cur = heap.poll(); - cur.timePoint += cur.workTime; - drinks[i] = cur.timePoint; - heap.add(cur); - } - return process(drinks, a, b, 0, 0); - } - - // 方法二,洗咖啡杯的方式和原来一样,只是这个暴力版本减少了一个可变参数 - - // process(drinks, 3, 10, 0,0) - // a 洗一杯的时间 固定变量 - // b 自己挥发干净的时间 固定变量 - // drinks 每一个员工喝完的时间 固定变量 - // drinks[0..index-1]都已经干净了,不用你操心了 - // drinks[index...]都想变干净,这是我操心的,washLine表示洗的机器何时可用 - // drinks[index...]变干净,最少的时间点返回 - public static int process(int[] drinks, int a, int b, int index, int washLine) { - if (index == drinks.length - 1) { - return Math.min(Math.max(washLine, drinks[index]) + a, drinks[index] + b); - } - // 剩不止一杯咖啡 - // wash是我当前的咖啡杯,洗完的时间 - int wash = Math.max(washLine, drinks[index]) + a;// 洗,index一杯,结束的时间点 - // index+1...变干净的最早时间 - int next1 = process(drinks, a, b, index + 1, wash); - // index.... - int p1 = Math.max(wash, next1); - int dry = drinks[index] + b; // 挥发,index一杯,结束的时间点 - int next2 = process(drinks, a, b, index + 1, washLine); - int p2 = Math.max(dry, next2); - return Math.min(p1, p2); - } - - public static int dp(int[] drinks, int a, int b) { - if (a >= b) { - return drinks[drinks.length - 1] + b; - } - // a < b - int N = drinks.length; - int limit = 0; // 咖啡机什么时候可用 - for (int i = 0; i < N; i++) { - limit = Math.max(limit, drinks[i]) + a; - } - int[][] dp = new int[N][limit + 1]; - // N-1行,所有的值 - for (int washLine = 0; washLine <= limit; washLine++) { - dp[N - 1][washLine] = Math.min(Math.max(washLine, drinks[N - 1]) + a, drinks[N - 1] + b); - } - for (int index = N - 2; index >= 0; index--) { - for (int washLine = 0; washLine <= limit; washLine++) { - int p1 = Integer.MAX_VALUE; - int wash = Math.max(washLine, drinks[index]) + a; - if (wash <= limit) { - p1 = Math.max(wash, dp[index + 1][wash]); - } - int p2 = Math.max(drinks[index] + b, dp[index + 1][washLine]); - dp[index][washLine] = Math.min(p1, p2); - } - } - return dp[0][0]; - } - - // 方法三:最终版本,把方法二洗咖啡杯的暴力尝试进一步优化成动态规划 - public static int minTime3(int[] arr, int n, int a, int b) { - PriorityQueue heap = new PriorityQueue(new MachineComparator()); - for (int i = 0; i < arr.length; i++) { - heap.add(new Machine(0, arr[i])); - } - int[] drinks = new int[n]; - for (int i = 0; i < n; i++) { - Machine cur = heap.poll(); - cur.timePoint += cur.workTime; - drinks[i] = cur.timePoint; - heap.add(cur); - } - if (a >= b) { - return drinks[n - 1] + b; - } - int[][] dp = new int[n][drinks[n - 1] + n * a]; - for (int i = 0; i < dp[0].length; i++) { - dp[n - 1][i] = Math.min(Math.max(i, drinks[n - 1]) + a, drinks[n - 1] + b); - } - for (int row = n - 2; row >= 0; row--) { // row 咖啡杯的编号 - int washLine = drinks[row] + (row + 1) * a; - for (int col = 0; col < washLine; col++) { - int wash = Math.max(col, drinks[row]) + a; - dp[row][col] = Math.min(Math.max(wash, dp[row + 1][wash]), Math.max(drinks[row] + b, dp[row + 1][col])); - } - } - return dp[0][0]; - } - - // for test - public static int[] randomArray(int len, int max) { - int[] arr = new int[len]; - for (int i = 0; i < len; i++) { - arr[i] = (int) (Math.random() * max) + 1; - } - return arr; - } - - // for test - public static void printArray(int[] arr) { - System.out.print("arr : "); - for (int j = 0; j < arr.length; j++) { - System.out.print(arr[j] + ", "); - } - System.out.println(); - } - - public static void main(String[] args) { - int[] test = { 1, 1, 5, 5, 7, 10, 12, 12, 12, 12, 12, 12, 15 }; - int a1 = 3; - int b1 = 10; - System.out.println(process(test, a1, b1, 0, 0)); - System.out.println(dp(test, a1, b1)); - - int len = 5; - int max = 9; - int testTime = 50000; - for (int i = 0; i < testTime; i++) { - int[] arr = randomArray(len, max); - int n = (int) (Math.random() * 5) + 1; - int a = (int) (Math.random() * 5) + 1; - int b = (int) (Math.random() * 10) + 1; - int ans1 = minTime1(arr, n, a, b); - int ans2 = minTime2(arr, n, a, b); - int ans3 = minTime3(arr, n, a, b); - if (ans1 != ans2 || ans2 != ans3) { - printArray(arr); - System.out.println("n : " + n); - System.out.println("a : " + a); - System.out.println("b : " + b); - System.out.println(ans1 + " , " + ans2 + " , " + ans3); - System.out.println("==============="); - break; - } - } - - } - -} -``` - - -## 1.11 如何分析有没有重复解 - -1、列出调用过程,可以只列出前几层 - -2、有没有重复解,一看便知 - -## 1.12 暴力递归到动态规划的套路 - -1、你已经有了一个不违反原则的暴力递归,而且的确存在重复调用 - -2、找到哪些参数的变化会影响返回值,对每一个列出变化范围 - -3、参数间的所有组合数量,意味着缓存表(DP)大小 - -4、记忆化搜索的方法就是傻缓存,非常容易得到 - -5、规定好严格的表大小,分析位置的依赖顺序,然后从基础填写到最终解 - -6、对于有枚举行为的决策过程,进一步优化 - -## 1.13 动态规划的进一步优化 - -> 主要就是用来优化动态规划的单个位置的枚举行为 - -1、空间压缩 - -2、状态化简 - -3、四边形不等式 - -4、其他优化技巧 diff --git "a/12-\346\232\264\345\212\233\351\200\222\345\275\222\345\210\260\345\212\250\346\200\201\350\247\204\345\210\222\344\274\230\345\214\226\346\200\235\350\267\257.md" "b/12-\346\232\264\345\212\233\351\200\222\345\275\222\345\210\260\345\212\250\346\200\201\350\247\204\345\210\222\344\274\230\345\214\226\346\200\235\350\267\257.md" new file mode 100644 index 0000000..7ea5cd8 --- /dev/null +++ "b/12-\346\232\264\345\212\233\351\200\222\345\275\222\345\210\260\345\212\250\346\200\201\350\247\204\345\210\222\344\274\230\345\214\226\346\200\235\350\267\257.md" @@ -0,0 +1,1054 @@ +[TOC] +# 1 暴力递归到动态规划 + +动态规划最核心的是暴力递归的尝试过程,一旦通过尝试写出来暴力递归的解法,那么动态规划在此基础上就比较好改了 + +> 1、暴力递归之所以暴力,是因为存在大量的重复计算。加入我们定义我们的缓存结构,用来查该状态有没有计算过,那么会加速我们的递归 + +> 2、在我们加入缓存结构之后,消除了大量的重复计算,缓存表就是我们的dp表。那么这种去除重复计算的递归,就是最粗糙的动态规划,也叫记忆化搜索 + +> 3、如果我们把我们的dp表,从简单到复杂列出来,那么就是经典的动态规划。我们无需考虑转移方程怎么写,而是根据我们的递归来推导。看下面例子: + +## 1.1 例一 : 机器人运动问题(2018阿里面试题目) + +> 认识暴力递归改动态规划的过程 + + +假设有排成一行的N个位置,记为1到N,N一定大于等于2。开始时机器人在其中的M位置上(M一定是1到N中的一个)。 + +如果机器人来到1位置,那么下一步只能往右来到2位置; + +如果机器人来到N位置,那么下一步只能往左来到N-1的位置; + +如果机器人来到中间位置,那么下一步可以往左走或者往右走; + +规定机器人必须走K步,最终能来到P位置(P也是1~N中的一个)的方法有多少种? + +给定四个参数N,M,K,P。返回方法数 + +> 暴力递归ways1调用的walk函数,就是暴力递归过程,存在重复计算。waysCache方法对应的walkCache就是在纯暴力递归的基础上加了缓存 + +假设我们的参数N=7,M=2,K=5,P=3。我们根据我们的递归加缓存的过程来填我们的dp表: + +1、int[][] dp = new int[N+1][K+1];是我们的DP表的范围 + +2、当rest==0的时候,dp[cur][rest] = cur == P ? 1 : 0; 我们只有cur==P的时候为1,其他位置都为0。 + +3、当cur==1的时候,dp[cur][rest] = walkCache(N, 2, rest - 1, P, dp); 我们的dp当前的值,依赖于cur的下一个位置,rest的上一个位置 + +4、当cur == N的时候,dp[cur][rest] = walkCache(N, N - 1, rest - 1, P,dp); 我们dp当前位置依赖于N-1位置,和rest - 1位置 + +5、当cur在任意中间位置时。dp[cur][rest] = walkCache(N, cur + 1, rest - 1, P,dp) + walkCache(N, cur - 1, rest - 1, P, dp); dp的当前位置依赖于dp的cur+1和rest-1位置加上dp的cur-1和rest-1的位置 + +那么我们可以得到我们的DP表为: + +```text + + 0 1 2 3 4 5 K 坐标 +0 x x x x x x + +1 0 0 1 0 3 0 + +2 0 1 0 3 0 9 + +3 1 0 2 0 6 0 + +4 0 1 0 3 0 10 + +5 0 0 1 0 5 0 + +6 0 0 0 1 0 5 + +7 0 0 0 0 1 0 + +cur +坐 +标 +``` + +> 所以任何的动态规划,都可以由暴力递归改出来。也就是说任意的动态规划都来自于某个暴力递归。反之任何暴力递归不一定能改成动态规划,假如某暴力递归并没有重复计算,没有缓存的必要 + +> 动态规划实质就是把参数组合完成结构化的缓存 + +```Go +package main + +import "fmt" + +// robotWay1 暴力递归版本 +func robotWay1(N, M, K, P int) int { + // 参数无效直接返回 + if N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N { + return 0 + } + + // 总共N个位置,从M点出发,还剩K步可以走,返回最终能达到P的方法数 + return walk(N, M, K, P) +} + +// N : 位置为1 ~ N,固定参数 +// cur : 当前在cur位置,可变参数 +// rest : 还剩res步没有走,可变参数 +// P : 最终目标位置是P,固定参数 +// 该函数的含义:只能在1~N这些位置上移动,当前在cur位置,走完rest步之后,停在P位置的方法数作为返回值返回 +func walk(N, cur, rest, P int) int { + // 如果没有剩余步数了,当前的cur位置就是最后的位置 + // 如果最后的位置停在P上,那么之前做的移动是有效的 + // 如果最后的位置没在P上,那么之前做的移动是无效的 + if rest == 0 { + if cur == P { + return 1 + } else { + return 0 + } + } + + // 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2 + // 后续的过程就是,来到2位置上,还剩rest-1步要走 + if cur == 1 { + return walk(N, 2, rest-1, P) + } + + // 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1 + // 后续的过程就是,来到N-1位置上,还剩rest-1步要走 + if cur == N { + return walk(N, N-1, rest-1, P) + } + + // 如果还有rest步要走,而当前的cur位置在中间位置上,那么当前这步可以走向左,也可以走向右 + // 走向左之后,后续的过程就是,来到cur-1位置上,还剩rest-1步要走 + // 走向右之后,后续的过程就是,来到cur+1位置上,还剩rest-1步要走 + // 走向左、走向右是截然不同的方法,所以总方法数要都算上 + return walk(N, cur+1, rest-1, P) + walk(N, cur-1, rest-1, P) +} + +// waysCache 记忆化搜索版本 +func waysCache(N, M, K, P int) int { + // 参数无效直接返回 + if N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N { + return 0 + } + + // 我们准备一张缓存的dp表 + // 由于我们的cur范围是1~N,我们准备N+1。 + // rest范围在1~K。我们准备K+1。 + // 目的是把我们的可能结果都能装得下 + dp := make([][]int, N+1) + for i := 0; i < N+1; i++ { + dp[i] = make([]int, K + 1) + } + + // 设置这张表的初始值都为-1,代表都还没用过 + for row := 0; row <= N; row++ { + for col := 0; col <= K; col ++ { + dp[row][col] = -1 + } + } + + return walkCache(N, M, K, P, dp) +} + +// HashMap (19,100) "19_100" +// 我想把所有cur和rest的组合,返回的结果,加入到缓存里 +func walkCache(N, cur, rest, P int, dp [][]int) int { + // 当前场景已经计算过,不要再暴力展开,直接从缓存中拿 + if dp[cur][rest] != -1 { + return dp[cur][rest] + } + if rest == 0 { + // 先加缓存 + if cur == P { + dp[cur][rest] = 1 + } else { + dp[cur][rest] = 0 + } + return dp[cur][rest] + } + if cur == 1 { + // 先加缓存 + dp[cur][rest] = walkCache(N, 2, rest - 1, P, dp) + return dp[cur][rest] + } + + if cur == N { + // 先加缓存 + dp[cur][rest] = walkCache(N, N - 1, rest - 1, P, dp) + return dp[cur][rest] + } + + // 先加缓存 + dp[cur][rest] = walkCache(N, cur + 1, rest - 1, P, dp) + walkCache(N, cur - 1, rest - 1, P, dp) + return dp[cur][rest] +} + +func ways3(N, M, K, P int) int { + // 参数无效直接返回0 + if N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N { + return 0 + } + dp := make([]int, N + 1) + dp[P] = 1 + for i := 1; i <= K; i++ { + leftUp := dp[1]// 左上角的值 + for j := 1; j <= N; j++ { + tmp := dp[j] + if j == 1 { + dp[j] = dp[j + 1] + } else if j == N { + dp[j] = leftUp + } else { + dp[j] = leftUp + dp[j + 1] + } + leftUp = tmp + } + } + return dp[M] +} + +// ways4 是我们很容易想到的递归解法 +func ways4(N, M, K, P int) int { + // 参数无效直接返回0 + if N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N { + return 0 + } + return process(N, 0, P, M, K) +} + +// 一共N个位置,从M点出发,一共只有K步。返回走到位置j,剩余步数为i的方法数 +func process(N, i, j, M, K int) int { + if i == K { + if j == M { + return 1 + } else { + return 0 + } + } + if j == 1{ + return process(N, i + 1, j + 1, M, K) + } + + if j == N { + return process(N, i + 1, j -1, M, K) + } + + return process(N, i + 1, j + 1, M, K) + process(N, i + 1, j - 1, M, K) +} + +// waysByDp 是我们在比较容易想到的递归解法之上,改写动态规划 +func waysByDp(N, M, K, P int) int { + // 参数无效直接返回0 + if N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N { + return 0 + } + // K + 1行 N + 1列 + dp := make([][]int, K + 1) + for i := 0; i < K + 1; i ++ { + dp[i] = make([]int, N + 1) + } + + dp[K][M] = 1 + for i := K - 1; i >= 0; i-- { + for j := 1; j <= N; j ++ { + if j == 1 { + dp[i][j] = dp[i + 1][j + 1] + } else if j == N { + dp[i][j] = dp[i + 1][j - 1] + } else { + dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j - 1] + } + } + } + return dp[0][P] +} + +func main() { + fmt.Println(robotWay1(7, 4, 9, 5)) + fmt.Println(waysCache(7, 4, 9, 5)) + fmt.Println(ways3(7, 4, 9, 5)) + fmt.Println(ways4(7, 4, 9, 5)) + fmt.Println(waysByDp(7, 4, 9, 5)) +} +``` + +## 1.2 例二:背包问题改动态规划 + +> 暴力递归过程各种,我们如何组织我们的决策过程。实质就是动态规划的状态转移 + +```Go +package main + +import ( + "fmt" + "math" +) + +// getMaxValue 原递归解法 +func getMaxValue(w, v []int, bag int) int { + // 主函数需要0 bag的返回值 + return bagProcess(w, v, 0, 0, bag) +} + +// index... 最大价值 +func bagProcess(w, v []int, index, alreadyW, bag int) int { + if alreadyW > bag { + return -1 + } + + // 重量没超 + if index == len(w) { + return 0 + } + + p1 := bagProcess(w, v, index + 1, alreadyW, bag) + p2Next := bagProcess(w, v, index + 1, alreadyW + w[index], bag) + p2 := -1 + if p2Next != -1 { + p2 = v[index] + p2Next + } + return int(math.Max(float64(p1), float64(p2))) +} + +// getMaxValue2 递归解法2 +func getMaxValue2 (w, v []int, bag int) int { + return bagProcess2(w, v, 0, bag) +} + +// 只剩下rest的空间了, +// index...货物自由选择,但是不要超过rest的空间 +// 返回能够获得的最大价值 +func bagProcess2(w, v []int, index, rest int) int { + if rest < 0 { // base case 1 + return -1 + } + + // rest >=0 + if index == len(w) { + return 0 + } + + // 有货也有空间 + p1 := bagProcess2(w, v, index + 1, rest) + p2 := -1 + p2Next := bagProcess2(w, v, index + 1, rest - w[index]) + if p2Next != -1 { + p2 = v[index] + p2Next + } + + return int(math.Max(float64(p1), float64(p2))) +} + +// getMaxValueDpWay 就是把上述的暴力递归改为动态规划 +func getMaxValueDpWay(w, v []int, bag int) int { + N := len(w) + // 准备一张dp表,行号为我们的重量范围。列为我们的价值范围 + // N + 1行 bag + 1列 + 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++ { // rest < 0 + // 通过正常位置的递归处理 + // 我们转而填写dp表格,注释位置是正常递归处理 + //p1 := bagProcess2(w, v, index + 1, rest) + //p2 := -1 + //p2Next := bagProcess2(w, v, index + 1, rest - w[index]) + //if p2Next != -1 { + // p2 = v[index] + p2Next + //} + // + //return int(math.Max(float64(p1), float64(p2))) + + // 所以我们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))) + } + } + return dp[0][bag] +} + +func main() { + weights := []int{3,2,4,7} + values := []int{5,6,3,19} + bag := 11 + fmt.Println(maxBagValue(weights,values,bag)) + fmt.Println(maxBagValueDp(weights,values,bag)) +} +``` + +## 1.3 动态规划解题思路 + +1、拿到题目先找到某一个暴力递归的写法(尝试) + +2、分析我们的暴力递归,是否存在重复解。没有重复解的递归没必要改动态规划 + +3、把暴力递归中的可变参数,做成缓存结构,不讲究组织。即没算过加入缓存结构,算过的直接拿缓存,就是记忆化搜索 + +4、如果把缓存结构做精细化组织,就是我们经典的动态规划 + +5、以背包问题举例,我们每一个重量有要和不要两个选择,且都要递归展开。那么我们的递归时间复杂度尾O(2^N)。而记忆化搜索,根据可变参数得到的长为N价值为W的二维表,那么我们的时间复杂度为O(N*bag)。如果递归过程中状态转移有化简继续优化的可能,例如枚举。那么经典动态规划可以继续优化,否则记忆化搜索和动态规划的时间复杂度是一样的 + +### 1.3.1 凑货币问题(重要) + +> 对动态规划完整优化路径的推演 + +有一个表示货币面值的数组arr,每种面值的货币可以使用任意多张。arr数组元素为正数,且无重复值。例如[7,3,100,50]这是四种面值的货币。 + +问:给定一个特定金额Sum,我们用货币面值数组有多少种方法,可以凑出该面值。例如P=1000,用这是四种面值有多少种可能凑出1000 + +> waysA为暴力递归的解题思路及实现 + +> waysB为暴力递归改记忆化搜索的版本 + +> waysC为记忆化搜索版本改动态规划的版本 + +```Go +package main + +import "fmt" + +// 凑货币问题 +// waysA为暴力递归的解题思路及实现 +// waysB为暴力递归改记忆化搜索的版本 +// waysC为记忆化搜索版本改动态规划的版本 +// waysD为动态规划的优化版本 + +// arr中都是正数且无重复值,返回组成aim的方法数,暴力递归 +func waysA(arr []int, aim int) int { + if len(arr) == 0 || aim < 0 { + return 0 + } + + return processCoinsA(arr, 0, aim) +} + +func processCoinsA(arr []int, index int, rest int) int { + // base case + // 当在面值数组的arr.length,此时越界,没有货币可以选择。 + // 如果当前目标金额就是0,那么存在一种方法,如果目标金额不为0,返回0中方法 + if index == len(arr) { + if rest == 0 { + return 1 + } else { + return 0 + } + } + + // 普遍位置 + ways := 0 + // 从0号位置开始枚举,选择0张,1张,2张等 + // 条件是张数乘以选择的面值,不超过面值rest + for count := 0; count * arr[index] <= rest; count++ { + // 方法数加上除去当前选择后所剩面额到下一位置的选择数,递归 + ways += processCoinsA(arr, index + 1, rest - (count * arr[index])) + } + + return ways +} + +// waysA暴力递归,改为记忆化搜索。ways2为记忆化搜索版本 +func waysB(arr []int, aim int) int { + if len(arr) == 0 || aim < 0 { + return 0 + } + + dp := make([][]int, len(arr) + 1) + for i := 0; i < len(arr) + 1; i++ { + dp[i] = make([]int, aim + 1) + } + + // 一开始所有的过程,都没有计算呢,dp二维表初始化为-1 + // dp[..][..] = -1 + for i := 0; i - 1 +func processCoinsB(arr []int, index, rest int, dp [][]int) int { + if dp[index][rest] != -1 { + return dp[index][rest] + } + + if index == len(arr) { + if rest == 0 { + dp[index][rest] = 1 + } else { + dp[index][rest] = 0 + } + return dp[index][rest] + } + + ways := 0 + + for count := 0; count * arr[index] <= rest; count++ { + // 返回之前加入缓存 + ways += processCoinsB(arr, index + 1, rest - (count * arr[index]), dp) + } + + // 返回之前加入缓存 + dp[index][rest] = ways + return ways +} + +// 记忆化搜索改造为动态规划版本,waysC。 +// 如果没有枚举行为,该动态该规划为自顶向下的动态规划和记忆化搜索等效,但这题存在枚举行为。 +func waysC(arr []int, aim int) int{ + if len(arr) == 0 || aim < 0 { + return 0 + } + + N := len(arr) + // dp表 + dp := make([][]int, N + 1) + for i := 0; i < len(arr) + 1; i++ { + dp[i] = make([]int, aim + 1) + } + + // 根据递归方法,N为arr的越界位置,但是我们的dp表定义的是N+1。 + // N位置,如果aim为0,则dp[N][0] = 1; + dp[N][0] = 1 // dp[N][1...aim] = 0; + + // 每个位置依赖自己下面的位置,那么我们从下往上循环 + for index := N - 1; index >= 0; index-- { + // rest从左往右 + for rest := 0; rest <= aim; rest++ { + ways := 0 + for count := 0; count * arr[index] <= rest; count++ { + ways += dp[index + 1][rest - (count * arr[index])] + } + dp[index][rest] = ways + } + } + // 最终我们需要[0,aim]位置的解 + return dp[0][aim] +} + +// waysD 动态规划的优化版本 +// 由于存在枚举行为,我们可以进一步优化我们的动态规划。waysD是优化的动态规划 +// 根据枚举,用具体化例子来找出规律,省掉枚举 +func waysD(arr []int, aim int) int{ + if len(arr) == 0 || aim < 0 { + return 0 + } + + N := len(arr) + // dp表 + dp := make([][]int, N + 1) + for i := 0; i < len(arr) + 1; i++ { + dp[i] = make([]int, aim + 1) + } + + dp[N][0] = 1 // dp[N][1...aim] = 0 + for index := N - 1; index >= 0; index-- { + for rest := 0; rest <= aim; rest++ { + dp[index][rest] = dp[index + 1][rest] + if rest - arr[index] >= 0 { + dp[index][rest] += dp[index][rest - arr[index]] + } + } + } + return dp[0][aim] +} + +func main() { + arr := []int{5, 10, 50, 100} + sum := 1000 + fmt.Println(waysA(arr, sum)) + fmt.Println(waysB(arr, sum)) + fmt.Println(waysC(arr, sum)) + fmt.Println(waysD(arr, sum)) +} +``` + +### 1.3.2 贴纸问题 + +给定一个字符串str,给定一个字符串类型的数组arr。arr里的每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来。 + +返回需要至少多少张贴纸可以完成这个任务。 + +例如:str="babac",arr={"ba","c","abcd"} + +至少需要两张贴纸"ba"和"abcd",因为使用这两张贴纸,把每一个字符串单独剪开,含有2个a,2个b,1个c。是可以拼出str的,所以返回2。 + +> 思路1 minStickers1:由于任何贴纸都可以剪切的很碎,跟贴纸的顺序没关系。那么目标str我们先进行排序,那么也不会有影响。babac排序为aabbc,我们再去选择贴纸,str被贴纸贴好后,剩下的接着排序,再选择...... + +> 由于我们的可变参数,只有一个目标字符串。但是目标字符串的可能性太多,没办法精细化动态规划。傻缓存的暴力递归已经是最优解了。所以只有一个可变参数,又存在重复解,那么傻缓存就是最优解。 + +> 思路2 minStickers2,我们每一张贴纸枚举所有可能的张数,后续过程不再考虑这张贴纸。但是方法一会好一点,因为只有一个可变参数。而方法二有两个可变参数,我们在设计递归的时候,尽量少的使用可变参数,这样我们缓存结构的命中率会提升 + +```Go +package main + +import ( + "fmt" + "math" +) + +// minStickers 贴纸问题,由于目标字符串可能性很多,没办法精细化动态规划,暴力递归加缓存dp即是最优解 +func minStickers(stickers []string, target string) int { + n := len(stickers) + + // stickers -> [26] [26] [26] + // 表示把每张贴纸转化为26个字母的词频数组 + m := make([][]int, n) + for i := 0; i < n; i++ { + m[i] = make([]int, 26) + } + + // 把每一张贴纸转化的字母26个字母有多少个 + for i := 0; i < n; i++ { + chars := []byte(stickers[i]) + for j := 0; j < len(chars); j++ { + m[i][chars[j]-'a']++ + } + } + + dp := make(map[string]int, 0) + dp[""] = 0 + return processStickers1(dp, m, target) +} + +// dp 傻缓存,如果rest已经算过了,直接返回dp中的值 +// rest 剩余的目标 +// 0..N每一个字符串所含字符的词频统计 +// 返回值是-1,map 中的贴纸 怎么都无法rest +func processStickers1(dp map[string]int, m [][]int, rest string) int { + if _, ok := dp[rest]; ok { + return dp[rest] + } + + // 以下就是正式的递归调用过程 + ans := math.MaxInt // ans -> 搞定rest,使用的最少的贴纸数量 + n := len(m) // N种贴纸 + tmp := make([]int, 26) // tmp 去替代 rest + // 把目标target剩余的rest进行词频统计 + target := []byte(rest) + for _, c := range target { + tmp[c-'a']++ + } + + // map表示所有贴纸的词频信息,tmp表示目标字符串的词频信息 + // 用map去覆盖tmp + for i := 0; i < n; i++ { + // 枚举当前第一张贴纸是谁? + // 第一张贴纸必须包含当前目标字符串的首字母。和所有贴纸去选等效 + // 目的是让当前贴纸的元素有在目标字符串的对应个,否则试过之后仍然是原始串,栈会溢出 + if m[i][target[0]-'a'] == 0 { + continue + } + + sb := make([]byte, 0) + // i 贴纸, j 枚举a~z字符 + for j := 0; j < 26; j++ { + if tmp[j] > 0 { // j这个字符是target需要的 + for k := 0; k < int(math.Max(float64(0), float64(tmp[j]-m[i][j]))); k++ { + // 添加剩余 + sb = append(sb, byte('a'+j)) + } + } + } + + // sb -> i 此时s就是当前贴纸搞定之后剩余的 + s := string(sb) + // processTmp是后续方案需要的贴纸数 + processTmp := processStickers1(dp, m, s) + if processTmp != -1 { + // 不是-1,最终返回当前后续贴纸数,加上当前贴纸数与ans的最小值 + ans = int(math.Min(float64(ans), float64(1+processTmp))) + } + } + + if ans == math.MaxInt { + dp[rest] = -1 + } else { + dp[rest] = ans + } + + return dp[rest] +} + +func main() { + arr := []string{"aaaa", "bbaa", "ccddd"} + str := "abcccccdddddbbbaaaaa" + fmt.Println(minStickers(arr, str)) +} +``` + +## 1.4 什么暴力递归可以继续优化? + +1、有重复调用同一个子问题的解,这种递归可以优化 + +2、如果每一个子问题都是不同的解,无法优化也不用优化 + +## 1.5 暴力地柜和动态规划的关系 + +1、某个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划 + +2、任何动态规划问题,都一定对应着某一个有解的重复调用的暴力递归 + +3、不是所有的暴力递归,都一定对应着动态规划 + +## 1.6 面试题中和动态规划的关系 + +1、解决一个问题,可能有很多尝试方法 + +2、可能在很多尝试方法中,又有若干个尝试方法有动态规划的方式 + +3、一个问题,可能有若干种动态规划的解法 + +## 1.7 如何找到某个问题的动态规划方式? + +1、设计暴力递归:重要原则+4中常见尝试模型!重点 + +2、分析有没有重复解:套路解决 + +3、用记忆化搜索 -> 用严格表结构实现动态规划:套路解决 + +4、看看能否继续优化:套路解决 + +## 1.8 面试中设计暴力递归过程的原则 + +怎么猜是不对的? + +1、每一个可变参数,一定不要比int类型更加复杂 + +> 比如可变参数是int a和int b 那么我们的缓存结构可以是a*b的二维数组。大小取决于a和b的范围 + +> 但是如果我们的可变参数是一个数组,int[] a。那么如果过多,我们不太容易设计那么大的缓存结构。如果只有一个这种可变参数就是原则2 + +> 不管什么问题,我们在脑海中想象可变参数就不会突破整形范围 + +2、原则1可以违反,让类型突破到一维线性结构,那必须是唯一可变参数。例如上述贴纸问题 + +3、如果发现原则1被违反,但没违反原则2。只需要做到记忆化搜索就是最优解 + +3、可变参数个数,能少则少 + + +## 1.9 笔试面试过程中怎么设计暴力递归? + +1、一定逼自己找到不违反1.8原则情况下的暴力尝试! + +2、如果你找到暴力尝试,不符合原则,马上舍弃,找新的! + +3、如果某个题目突破了设计原则,一定极难极难,面试中出现的概率低于5% + +## 1.10 常见的4种尝试模型 + +> 每一个模型背后都有很多题目 + +### 1.10.1 从左往右的尝试模型 + +> 例如背包问题 + +### 1.10.2 范围上的尝试模型 + +> 例如纸牌博弈问题 + +### 1.10.3 多样本位置全对应的尝试模型 + +#### 1.10.3.1 两个字符串的最长公共子序列问题 + +> 例如“ab1cd2ef345gh”和“opq123rs4tx5yz”的最长公共子序列为“12345”。即在两个字符串所有相等的子序列里最长的。所以返回子序列的长度5 + +> 假如"a123bc"和"12dea3fz"两个字符串,我们把这两个样本的下标一个作为行,一个作为列。观察这样的结构所表示的意义。dp表这样设计就是str1从0到i位置,str2从0到j位置,两个位置的最长公共子序列。 + +> dp[i][j] 表示的含义是,str1字符串在i位置, +str2在j位置,两个最长公共子序列多长。 +那么str1和str2的最长公共子序列,就是dp[str1.length][str2.length] + +> 对于任何位置dp[i][j]: + +> 1. 如果str1的i位置和str2的j位置的最长公共子序列跟str1的i位置字符和str2的j位置字符无关,那么此时的最长公共子序列长度就是dp[i-1][j-1] + +> 2. 此时与str1的i位置结尾的字符有关系,和str2的j位置结尾的字符没关系。此时跟str2的j位置没关系,最长公共子序列式dp[i][j-1] + +> 3. 此时与str1的i位置结尾的字符没关系,和str2的j位置结尾的字符有关系。此时跟str1的j位置没关系,最长公共子序列式dp[i-1][j] + +> 4. 此时即与str1的i字符结尾,有关系。又与str2的j位置结尾,有关系。只有str1[i]==str2[j]才有可能存在这种情况,且为dp[i-1][j-1] + 1 + + +```text + + 0 1 2 3 4 5 6 7 str2 +0 0 0 0 0 1 1 1 1 + +1 0 + +2 1 + +3 1 + +4 1 + +5 1 + +str1 + +``` + +```Go +package main + +import "math" + +// lcse 给定两个字符串,求两个字符串的最长公共子序列的长度 +func lcse(str1, str2 string) int { + chars1 := []byte(str1) + chars2 := []byte(str2) + + dp := make([][]int, len(chars1)) + for i := 0; i < len(chars1); i++ { + dp[i] = make([]int, len(chars2)) + } + + if chars1[0] == chars2[0] { + dp[0][0] = 1 + } else { + dp[0][0] = 0 + } + + // 填第0列的所有值 + // 一旦st1r的i位置某个字符等于str2的0位置,那么之后都是1 + for i := 1; i < len(chars1); i++ { + tmp := -1 + if chars1[i] == chars2[0] { + tmp = 1 + } else { + tmp = 0 + } + dp[i][0] = int(math.Max(float64(dp[i-1][0]), float64(tmp))) + } + + // 填第0行的所有值 + // 一旦str2的j位置某个字符等于str1的0位置,那么之后都是1 + for j := 1; j < len(chars2); j++ { + tmp := -1 + if chars1[0] == chars2[j] { + tmp = 1 + } else { + tmp = 0 + } + + dp[0][j] = int(math.Max(float64(dp[0][j - 1]), float64(tmp))) + } + + for i := 1; i < len(chars1); i++ { + for j := 1; j < len(chars2); 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 chars1[i] == chars2[i] { + dp[i][j] = int(math.Max(float64(dp[i][j]), float64(dp[i - 1][j - 1] + 1))) + } + } + } + return dp[len(chars1) - 1][len(chars2) - 1] +} +``` + +### 1.10.4 寻找业务限制的尝试模型 + +> 四种模型中最难的一种,暂时只看process方法,process更改为动态规划的dp方法。后面会展开说明其他方法, + +例题:给定一个数组,代表每个人喝完咖啡准备刷杯子的时间,只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯。每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发。返回让所有咖啡杯变干净的最早完成时间。三个参数:int[]arr , int a , int b(京东) + + +```Go +package main + +import "math" + +// 数组arr代表每一个咖啡机冲一杯咖啡的时间,每个咖啡机只能串行的制造咖啡。 +// 现在有n个人需要喝咖啡,只能用咖啡机来制造咖啡。 +// 认为每个人喝咖啡的时间非常短,冲好的时间即是喝完的时间。 +// 每个人喝完之后咖啡杯可以选择洗或者自然挥发干净,只有一台洗咖啡杯的机器,只能串行的洗咖啡杯。 +// 洗杯子的机器洗完一个杯子时间为a,任何一个杯子自然挥发干净的时间为b。 +// 四个参数:arr, n, a, b +// 假设时间点从0开始,返回所有人喝完咖啡并洗完咖啡杯的全部过程结束后,至少来到什么时间点。 + +// 方法二,每个人暴力尝试用每一个咖啡机给自己做咖啡,优化成贪心 +func minTime2(arr []int, n, a, b int) int { + heap := &Machines{ + Machines: make([]*Machine, 0), + } + + // 构造洗碗机加入堆中 + for i := 0; i < len(arr); i++ { + heap.Push(&Machine{ + TimePoint: 0, + WorkTime: arr[i], + }) + } + + drinks := make([]int, n) + for i:=0; i= b { + return drinks[n - 1] + b + } + + dp := make([][]int, n) + for i := 0; i < n; i++ { + dp[i] = make([]int, drinks[n - 1] + n * a) + } + + for i := 0; i < len(dp[0]); i++ { + dp[n - 1][i] = int(math.Min(math.Max(float64(i), float64(drinks[n - 1])) + float64(a), float64(drinks[n - 1] + b))) + } + + for row := n - 2; row >= 0; row-- { // row 咖啡杯的编号 + washLine := drinks[row] + (row + 1) * a + for col := 0; col < washLine; col++ { + wash := int(math.Max(float64(col), float64(drinks[row]))) + a + dp[row][col] = int(math.Min(math.Max(float64(wash), float64(dp[row + 1][wash])), + math.Max(float64(drinks[row] + b), float64(dp[row + 1][row])))) + } + } + + return dp[0][0] +} +``` + + +## 1.11 如何分析有没有重复解 + +1、列出调用过程,可以只列出前几层 + +2、有没有重复解,一看便知 + +## 1.12 暴力递归到动态规划的套路 + +1、你已经有了一个不违反原则的暴力递归,而且的确存在重复调用 + +2、找到哪些参数的变化会影响返回值,对每一个列出变化范围 + +3、参数间的所有组合数量,意味着缓存表(DP)大小 + +4、记忆化搜索的方法就是傻缓存,非常容易得到 + +5、规定好严格的表大小,分析位置的依赖顺序,然后从基础填写到最终解 + +6、对于有枚举行为的决策过程,进一步优化 + +## 1.13 动态规划的进一步优化 + +> 主要就是用来优化动态规划的单个位置的枚举行为 + +1、空间压缩 + +2、状态化简 + +3、四边形不等式 + +4、其他优化技巧 diff --git "a/13-\343\200\212\350\277\233\351\230\266\343\200\213\345\215\225\350\260\203\346\240\210\345\222\214\347\252\227\345\217\243.md" "b/13-\343\200\212\350\277\233\351\230\266\343\200\213\345\215\225\350\260\203\346\240\210\345\222\214\347\252\227\345\217\243.md" new file mode 100644 index 0000000..2e0829f --- /dev/null +++ "b/13-\343\200\212\350\277\233\351\230\266\343\200\213\345\215\225\350\260\203\346\240\210\345\222\214\347\252\227\345\217\243.md" @@ -0,0 +1,499 @@ +[TOC] +# 1 单调栈和窗口及其更新结构 + +## 1.1 窗口 + +### 1.1.1 滑动窗口是什么? + +> 窗口只是我们脑海中的一个范围,用过L和R来规定我们窗口的边界。保证L<=R这个条件 + +1、滑动窗口是一种想象出来的数据结构; + +2、滑动窗口有左边界L和右边界R + +3、在数组或者字符串或者一个序列上,记为S,窗口就是S[L...R]这一部分 + +4、L往右滑动意味着一个样本出了窗口,R往右滑意味着一个样本进了窗口 + +5、 L和R都只能往右滑动 + + +### 1.1.2 滑动窗口能做什么? + +滑动窗口、首尾指针等技巧,说白了就是一种求解问题的流程设计。 + +### 1.1.3 维护窗口滑动的更新结构 + +> 例如我们求窗口内最大值问题 + +> 用单调双端队列来实现,双端队列就是我们的双向链表结构。我们保证我们的双端队列从头部到尾部数值是**从大到小**的 + +1、 如果窗口的R位置往右移动,我们把进入窗口的这个数从尾部加入到双端队列。如果当前数比该数的前一个数大(从尾部看)那么队列中小于等于的数弹出。直到小于队列的前一个数,加入该数。 + +2、如果窗口的L位置往右移动,预示着有数要出我们的窗口,我们从双端队列的头部观看要出去的数是不是我们头部队列的数。是就弹出头部的数,不是就不做任何操作 + +3、我们窗口结构一直被维护,双端队列右边进,左边出。那么窗口的最大值就是我们双端队列最左侧(头部)的值 + +双端队列结构实质上指的是:如果此时形成的窗口状况,不想让R往右动了,而让L往右动。谁会以此成为最大值的优先级。为什么弹出的数不再找回,原因是在窗口滑动的过程中,被弹出的数的优先级已经被后来的大数取代了,这就是尾端加入,前一个数比当前数小则弹出,比当前数大就加入当前数的道理 + + +> 反之,如果我们要窗口内最小值,只需要维护我们的双端队列单调递增的,既由小到大的即可 + +复杂度:窗口滑动经过的数,最多进双端队列一次,最多出双端队列一次,如果窗口滑动了N个数,时间复杂度就是O(N),单次平均O(1)。 + + +### 1.1.4 高频题:求滑动窗口最大值 + +假设一个固定大小为W的窗口,依次划过arr,返回每一次划出状况的最大值 + +例如,`arr=[4, 3, 5, 4, 3, 3, 6, 7]` + +返回:`[5, 5, 5, 4, 6, 7]` + +分析:窗口起始是4,3,5,窗口内最大值是5。窗口向右滑动变为3,5,4最大值5...... + +```Go +package main + +import ( + "container/list" + "fmt" +) + +// getMaxWindow arr 是数据样本 w是窗口大小 +func getMaxWindow(arr []int, w int) []int { + if len(arr) == 0 || w < 1 || len(arr) < w { + return nil + } + + // 双向链表 + // 其中放的是下标位置,头代表 (大->小)尾 + qmax := list.List{} + // 窗口在滑动的过程中,最终会生成arr长度-窗口起始宽度+1个值 + res := make([]int, len(arr) - w + 1) + index := 0 + // 双端队列的头部元素 + var fe = &list.Element{} + // 双端队列的尾部元素 + var ee = &list.Element{} + // L...R + // R + for R := 0; R < len(arr); R++ { // 当前让 i -> [i] 进窗口 , i 就是 r + // R 位置的值 可以放在比他大的数后,或者空 + // 双端队列不为空,且双端队列尾部的值小于当前要进入窗口的值 + for qmax.Len() != 0 && arr[qmax.Back().Value.(int)] <= arr[R] { + // 双端队列从尾部弹出 + qmax.Remove(ee) + if qmax.Len() > 0 { + ee = qmax.Back() + fe = qmax.Front() + } + } + // 经过上述的while,最终把当前进入窗口的数放入双端队列的尾部 + qmax.PushBack(R) + fe = qmax.Front() + ee = qmax.Back() + // 数进来了 + // 如果窗口没有形成W的长度之前,不弹出数字的 + // 当前下标是R, R-W就是需要过期的下标。 + // 如果双端队列的头部保存的下标等于R-W,就头部弹出。实质R-W就是我们原始结构的L下标 + if qmax.Front().Value.(int) == R - w { + qmax.Remove(fe) + } + + // 以上窗口更新做完了 + // 窗口没有形成W长度之前,不收集答案。形成W长度后,每一次收集一个答案 + if R >= w -1 { + res[index] = arr[qmax.Front().Value.(int)] + index++ + } + } + return res +} + +func main() { + arr := []int{4, 3, 5, 4, 3, 3, 6, 7} + w := 3 + fmt.Println(getMaxWindow(arr, w)) +} +``` + +### 1.1.5 高频题二:达标子数组数量问题 + +给定一个整形数组arr,和一个整数num。某个arr中的子数组sub,如果想达标,必须满足: sub中最大值-sub中最小值<=num,返回arr中达标子数组的数量 + +> 子数组是连续的 + +> 结论1:对于[L,R]范围达标,那么[L,R]上的子数组都达标。max[L...R]肯定比其子数组的max要大,min[L...R]肯定比其范围内的子数组要小,那么[L...R]上满足max - min < num,则其子数组必定满足 + +> 同理可得结论2:对于[L...R]范围不达标,那么扩展范围后的[L'...R']也不达标 + +> 我们建立两个双端队列,一个是窗口最大值的双端队列,一个是窗口最小值的双端队列。我们扩展我们的窗口R加1,每扩展一个判断是否仍然达标,达标继续扩展,不达标就停,可以得到本次子数组的达标情况,接着缩小我们的窗口L加1,继续...。窗口滑动不会回退,整体O(N) + +```Go +package main + +import ( + "container/list" + "fmt" +) + +// 达标子数组数量问题 + +// getNum 给定样本数组,和一个目标数值num。求arr中达标的子数组,达标的要求为子数组中最大值减去子数组中最小值小于等于num +func getNum(arr []int, num int) int { + if len(arr) == 0 { + return 0 + } + + // 窗口内最小值的更新结构 + qmin := list.List{} + // 窗口内的最大值的更新结构 + qmax := list.List{} + + // 最小值双端队列的头部元素 + var minfe = &list.Element{} + // 最小值双端队列的尾部元素 + var minee = &list.Element{} + + // 最大值双端队列的头部元素 + var maxfe = &list.Element{} + // 最小值双端队列的尾部元素 + var maxee = &list.Element{} + + L := 0 + R := 0 + // [L..R) -> [0,0) 窗口内无数 [1,1) + // [0,1) -> [0~0] 窗口里只有一个数 + res := 0 + // L是开头位置,尝试每一个开头 + for L < len(arr) { + // 如果此时窗口的开头是L,下面的for工作是:R向右扩到违规为止 + + // R是最后一个达标位置的再下一个,通过下文的break终止 + for R < len(arr) { + // R位置的数进入窗口后,最小值的更新结构和最大值的更新结构都要更新 + for qmin.Len() != 0 && arr[qmin.Back().Value.(int)] >= arr[R] { + // 尾部移除 + qmin.Remove(minee) + if qmin.Len() > 0 { + minfe = qmin.Front() + minee = qmin.Back() + } + } + + qmin.PushBack(R) + minee = qmin.Back() + minfe = qmin.Front() + + // R -> arr[R], + for qmax.Len() != 0 && arr[qmax.Back().Value.(int)] <= arr[R] { + // 尾部移除 + qmax.Remove(maxee) + if qmax.Len() > 0 { + maxfe = qmax.Front() + maxee = qmax.Back() + } + } + + qmax.PushBack(R) + maxee = qmax.Back() + maxfe = qmax.Front() + + if arr[qmax.Front().Value.(int)]-arr[qmin.Front().Value.(int)] > num { + break + } + R++ + } + // R是最后一个达标位置的再下一个,第一个违规的位置 + res += R - L + // 检查最小值和最大值的更新结构有没有过期 + if qmin.Front().Value.(int) == L { + qmin.Remove(minfe) + if qmin.Len() > 0 { + minfe = qmin.Front() + minee = qmin.Back() + } + } + + if qmax.Front().Value.(int) == L { + qmax.Remove(maxfe) + if qmax.Len() > 0 { + maxfe = qmax.Front() + maxee = qmax.Back() + } + } + // 窗口左边界向右滑动,窗口容量此时减1 + L++ + } + return res +} + +func main() { + arr := []int{4, 2, 1, 5, 6, 1, 7, 22, 53, 16, 24, 65, 72, 17, 21, 42} + num := 5 + fmt.Println(getNum(arr, num)) +} +``` + +> 本题根据窗口滑动建立了单调性,上文的结论 + +### 1.1.6 如何优化一个问题? + +1、 数据状况层面是否可以优化 + +2、 问题本身是否可以优化。单调性,首尾指针(头指针往右走,尾指针往左走)等 + +> 遇到一个问题我们先观察,问题本身和范围是否可以建立单调性,至于需要用哪个原型来解决,窗口和首位指针法是常见的流程。所以窗口和首位指针主要用来解决单调性的 + +## 1.2 单调栈 + +### 1.2.1 单调栈结构 + +在一个数组中,求每一个位置左边离它最近的比它小的数在哪,右边离它最近的比它小的数在哪。 + +例如[3, 2, 1, 7]。3左边比它小的最近的位置的数的位置没有,为-1,右边是1位置的2。2左边比它小的最近的位置的数的位置没有,为-1,右边是2位置的1等。 + +用一个map来记录,暴力解O(N^2)。单调栈能够在O(N)解决 + + +单调栈算法流程: + +> 草稿纸上模拟这个栈; + +> 没有相等元素的情况:准备一个栈结构,暂定从小到大的单调栈结构。从左往右遍历我们的数组,[3,4,2,5],由于栈空,第一个元素可以进栈,3进栈。 + +> 1位置的数4可以进栈,因为没有破坏从小到大的栈的单调性。 + +> 2位置的2无法直接进栈,因为会破坏栈的单调性。需要弹出栈元素,元素一旦被弹出,生成相应的记录。 + +> 1位置的4弹出,右边最近的比你小的数,就是谁让你弹出的数,所以4的右边最近的比4小的数是2。左边最近比你小的数,就是你在栈中压着的数,所以4的左边最近的比4小的数是3。 + +> 2位置的2此时仍然无法进栈,因为栈中此时还有3,那么3弹出。3的最近的右侧比3小的数是2,3是栈底元素,没有压着的元素,所以3左侧最近的比3小的数没有,位置置为-1。其他元素同理......。 + +> 最后如果没有元素了,栈中元素弹出,此时不是其他元素迫使的弹出,所以自然弹出的右侧最近比它小的无返回-1。左侧最近比它小的看它在栈中是否压着其他元素 + +可以选择任意位置去证明,证明略 + +> 如果存在相等元素的情况,我们栈中每个元素保存为list表示相等元素列表。无法直接进入单调栈时,弹出list最右侧的元素,该元素右侧最近的比自己小的数,就是迫使它弹出的那个数。该元素左侧最近比它小的数,就是自身的这个list压着的的list的最右的数。list的相同元素有两种情况,一种是两个数相等且挨着,另外一种是某个位置释放了中间位置的数后遇到相等元素,进入一个list中去。画栈模拟可看出 + + +```Go +package main + +import "fmt" + +// 单调栈 + +// 数组中没有重复值的情况 +func getNearLessNoRepeat(arr []int) [][]int { + res := make([][]int, len(arr)) + for i := 0; i < len(arr); i++ { + res[i] = make([]int, 2) + } + + // 模拟一个栈 + stack := make([]int, 0) + for i := 0; i < len(arr); i++ { + for len(stack) != 0 && arr[stack[len(stack) - 1]] > arr[i] { + // 出栈 stack pop + popIndex := stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + + leftLessIndex := 0 + if len(stack) == 0 { + leftLessIndex = -1 + } else { + leftLessIndex = stack[len(stack) - 1] + } + + res[popIndex][0] = leftLessIndex + res[popIndex][1] = i + } + // stack push i + stack = append(stack, i) + } + + for len(stack) != 0 { + // 出栈 stack pop + popIndex := stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + + leftLessIndex := 0 + if len(stack) == 0 { + leftLessIndex = -1 + } else { + // leftLessIndex = stack peek + leftLessIndex = stack[len(stack) - 1] + } + + res[popIndex][0] = leftLessIndex + res[popIndex][1] = -1 + } + + return res +} + +// arr [3, 2, 1, 4, 5] +// 0 1 2 3 4 + +// 表示 0这个数左边最近比0小的没有,位置是-1,右边1。1位置数左边最近比0小的没有-1,右边2 +// [ +// 0 : [-1, 1 ] +// 1 : [-1, 2 ] + +// ] +// 数组中存在重复值的情况 +func getNearLess(arr []int) [][]int { + res := make([][]int, len(arr)) + for i := 0; i < len(arr); i++ { + res[i] = make([]int, 2) + } + + // []int -> 放的是位置,同样值的东西,位置压在一起 + // 代表值 底 -> 顶 小 -> 大 + stack := make([][]int, 0) + + for i := 0; i < len(arr); i++ { // i -> arr[i] 进栈 + // 栈底 -> 栈顶, 小 -> 大 + for len(stack) != 0 && arr[stack[len(stack) - 1][0]] > arr[i] { + // 出栈 stack pop + popIs := stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + + // 取位于下面位置的列表中,最晚加入的那个 + leftLessIndex := 0 + if len(stack) == 0 { + leftLessIndex = -1 + } else { + leftLessIndex = stack[len(stack) - 1][len(stack[len(stack) - 1]) - 1] + } + + for _,popi := range popIs { + res[popi][0] = leftLessIndex + res[popi][1] = i + } + } + + // 相等的、比你小的 + if len(stack) != 0 && arr[stack[len(stack) - 1][0]] == arr[i] { + stack[len(stack) - 1] = append(stack[len(stack) - 1], i) + } else { + list := make([]int, 0) + list = append(list, i) + // stack push + stack = append(stack, list) + } + } + + for len(stack) != 0 { + // stack pop + popIs := stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + + // 取位于下面位置的列表中,最晚加入的那个 + leftLessIndex := 0 + if len(stack) == 0 { + leftLessIndex = -1 + } else { + leftLessIndex = stack[len(stack) - 1][len(stack[len(stack) - 1]) - 1] + } + + for _,popi := range popIs { + res[popi][0] = leftLessIndex + res[popi][1] = -1 + } + } + return res +} + +func main() { + arr := []int{4, 3, 5, 6, 7} + fmt.Println(getNearLessNoRepeat(arr)) + arr = []int{4, 3, 5, 4, 3, 3, 6, 7} + fmt.Println(getNearLess(arr)) +} +``` + +### 1.2.2 单调栈的应用 + +给定一个只包含正整数的数组arr,arr中任何一个子数组sub,一定都可以算出(sub累加和)*(sub中的最小值)是什么,那么所有子数组中,这个值最大是多少? + +```Go +package main + +import ( + "fmt" + "math" +) + +// 给定一个正整数数组,求子数组中,累加和乘以数组中最小值。所有子数组中算出的最大值返回 +func max1(arr []int) int { + max := math.MinInt + for i := 0; i < len(arr); i++ { + for j := i; j < len(arr); j++ { + minNum := math.MaxInt + sum := 0 + for k := i; k <= j; k++ { + sum += arr[k] + minNum = int(math.Min(float64(minNum), float64(arr[k]))) + } + max = int(math.Max(float64(max), float64(minNum * sum))) + } + } + return max +} + +func max2(arr []int) int { + size := len(arr) + sums := make([]int, size) + + sums[0] = arr[0] + for i := 1; i < size; i++ { + sums[i] = sums[i - 1] + arr[i] + } + + max := math.MinInt + stack := make([]int, 0) + + for i := 0; i < size; i++ { + for len(stack) != 0 && arr[stack[len(stack) - 1]] >= arr[i] { + // stack pop + j := stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + + m := math.MinInt + if len(stack) == 0 { + m = sums[i - 1] + } else { + m = sums[i - 1] - sums[stack[len(stack) - 1]] + } + + max = int(math.Max(float64(max), float64(m * arr[j]))) + } + stack = append(stack, i) + } + + for len(stack) != 0 { + j := stack[len(stack) - 1] + stack = stack[:len(stack) - 1] + + m := math.MinInt + if len(stack) == 0 { + m = sums[size - 1] + } else { + m = sums[size - 1] - sums[stack[len(stack) - 1]] + } + + max = int(math.Max(float64(max), float64(m * arr[j]))) + } + + return max +} + +func main() { + arr := []int{3, 4, 1, 7, 3, 9, 12, 62, 82, 91, 30} + fmt.Println(max1(arr)) + fmt.Println(max2(arr)) +} +``` diff --git "a/13-\343\200\212\350\277\233\351\230\266\343\200\213\345\215\225\350\260\203\346\240\210\345\222\214\347\252\227\345\217\243\345\217\212\345\205\266\346\233\264\346\226\260\347\273\223\346\236\204.md" "b/13-\343\200\212\350\277\233\351\230\266\343\200\213\345\215\225\350\260\203\346\240\210\345\222\214\347\252\227\345\217\243\345\217\212\345\205\266\346\233\264\346\226\260\347\273\223\346\236\204.md" deleted file mode 100644 index 3e0c4fd..0000000 --- "a/13-\343\200\212\350\277\233\351\230\266\343\200\213\345\215\225\350\260\203\346\240\210\345\222\214\347\252\227\345\217\243\345\217\212\345\205\266\346\233\264\346\226\260\347\273\223\346\236\204.md" +++ /dev/null @@ -1,575 +0,0 @@ -[TOC] -# 1 单调栈和窗口及其更新结构 - -## 1.1 窗口 - -### 1.1.1 滑动窗口是什么? - -> 窗口只是我们脑海中的一个范围,用过L和R来规定我们窗口的边界。保证L<=R这个条件 - -1、滑动窗口是一种想象出来的数据结构; - -2、滑动窗口有左边界L和右边界R - -3、在数组或者字符串或者一个序列上,记为S,窗口就是S[L...R]这一部分 - -4、L往右滑动意味着一个样本出了窗口,R往右滑意味着一个样本进了窗口 - -5、 L和R都只能往右滑动 - - -### 1.1.2 滑动窗口能做什么? - -滑动窗口、首尾指针等技巧,说白了就是一种求解问题的流程设计。 - -### 1.1.3 维护窗口滑动的更新结构 - -> 例如我们求窗口内最大值问题 - -> 用单调双端队列来实现,双端队列就是我们的双向链表结构。我们保证我们的双端队列从头部到尾部数值是**从大到小**的 - -1、 如果窗口的R位置往右移动,我们把进入窗口的这个数从尾部加入到双端队列。如果当前数比该数的前一个数大(从尾部看)那么队列中小于等于的数弹出。直到小于队列的前一个数,加入该数。 - -2、如果窗口的L位置往右移动,预示着有数要出我们的窗口,我们从双端队列的头部观看要出去的数是不是我们头部队列的数。是就弹出头部的数,不是就不做任何操作 - -3、我们窗口结构一直被维护,双端队列右边进,左边出。那么窗口的最大值就是我们双端队列最左侧(头部)的值 - -==双端队列结构实质上指的是:如果此时形成的窗口状况,不想让R往右动了,而让L往右动。谁会以此成为最大值的优先级。为什么弹出的数不再找回,原因是在窗口滑动的过程中,被弹出的数的优先级已经被后来的大数取代了,这就是尾端加入,前一个数比当前数小则弹出,比当前数大就加入当前数的道理== - - -> 反之,如果我们要窗口内最小值,只需要维护我们的双端队列单调递增的,既由小到大的即可 - -复杂度:窗口滑动经过的数,最多进双端队列一次,最多出双端队列一次,如果窗口滑动了N个数,时间复杂度就是O(N),单次平均O(1)。 - - -### 1.1.4 高频题:求滑动窗口最大值 - -假设一个固定大小为W的窗口,以此划过arr,返回每一次划出状况的最大值 - -例如,`arr=[4, 3, 5, 4, 3, 3, 6, 7]` - -返回:`[5, 5, 5, 4, 6, 7]` - -分析:窗口起始是4,3,5,窗口内最大值是5。窗口向右滑动变为3,5,4最大值5...... - -```Java - -package class01; - -import java.util.LinkedList; - -public class Code01_SlidingWindowMaxArray { - - public static int[] getMaxWindow(int[] arr, int w) { - if (arr == null || w < 1 || arr.length < w) { - return null; - } - - // Java中LinkedList就是双端队列,双向链表 - // 其中放的是下标位置,头代表 (大->小)尾 - LinkedList qmax = new LinkedList(); - // 窗口在滑动的过程中,最终会生成arr长度-窗口起始宽度+1个值 - int[] res = new int[arr.length - w + 1]; - int index = 0; - // L...R - // R - for (int R = 0; R < arr.length; R++) { // 当前让 i -> [i] 进窗口 , i 就是 r - // R 位置的值 可以放在比他大的数后,或者空 - // 双端队列不为空,且双端队列尾部的值小于当前要进入窗口的值 - while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[R]) { - // 双端队列从尾部弹出 - qmax.pollLast(); - } - // 经过上述的while,最终把当前进入窗口的数放入双端队列的尾部 - qmax.addLast(R); - // 数进来了 - // 如果窗口没有形成W的长度之前,不弹出数字的 - // 当前下标是R, R-W就是需要过期的下标。 - // 如果双端队列的头部保存的下标等于R-W,就头部弹出。实质R-W就是我们原始结构的L下标 - if (qmax.peekFirst() == R - w) { - qmax.pollFirst(); - } - // 以上窗口更新做完了 - // 窗口没有形成W长度之前,不收集答案。形成W长度后,每一次收集一个答案 - if (R >= w - 1) { - res[index++] = arr[qmax.peekFirst()]; - } - } - return res; - } - - // for test - public static int[] rightWay(int[] arr, int w) { - if (arr == null || w < 1 || arr.length < w) { - return null; - } - int[] res = new int[arr.length - w + 1]; - int index = 0; - int L = 0; - int R = w - 1; - while (R < arr.length) { - int max = arr[L]; - for (int i = L + 1; i <= R; i++) { - max = Math.max(max, arr[i]); - - } - res[index++] = max; - L++; - R++; - } - return res; - } - - // for test - public static int[] generateRandomArray(int maxSize, int maxValue) { - int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) (Math.random() * (maxValue + 1)); - } - return arr; - } - - // for test - public static boolean isEqual(int[] arr1, int[] arr2) { - if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { - return false; - } - if (arr1 == null && arr2 == null) { - return true; - } - if (arr1.length != arr2.length) { - return false; - } - for (int i = 0; i < arr1.length; i++) { - if (arr1[i] != arr2[i]) { - return false; - } - } - return true; - } - - public static void main(String[] args) { - int testTime = 100000; - int maxSize = 100; - int maxValue = 100; - System.out.println("test begin"); - for (int i = 0; i < testTime; i++) { - int[] arr = generateRandomArray(maxSize, maxValue); - int w = (int) (Math.random() * (arr.length + 1)); - int[] ans1 = getMaxWindow(arr, w); - int[] ans2 = rightWay(arr, w); - if (!isEqual(ans1, ans2)) { - System.out.println("Oops!"); - } - } - System.out.println("test finish"); - } - -} - -``` - -### 1.1.5 高频题二:达标子数组数量问题 - -给定一个整形数组arr,和衣蛾整数num。某个arr中的子数组sub,如果想达标,必须满足:sub中最大值-sub中最小值<=num,返回arr中达标子数组的数量 - -> 子数组是连续的 - -> 结论1:对于[L...R]范围达标,那么[L...R]上的子数组都达标。max[L...R]肯定比其子数组的max要大,min[L...R]肯定比其范围内的子数组要小,那么[L...R]上满足max - min < num,则其子数组必定满足 - -> 同理可得结论2:对于[L...R]范围不达标,那么扩展范围后的[L'...R']也不达标 - -> 我们建立两个双端队列,一个是窗口最大值的双端队列,一个是窗口最小值的双端队列。我们扩展我们的窗口R加1,每扩展一个判断是否仍然达标,达标继续扩展,不达标就停,可以得到本次子数组的达标情况,接着缩小我们的窗口L加1,继续...。窗口滑动不会回退,整体O(N) - -```Java -package class01; - -import java.util.LinkedList; - -public class Code02_AllLessNumSubArray { - - public static int getNum(int[] arr, int num) { - if (arr == null || arr.length == 0) { - return 0; - } - // 窗口内最小值的更新结构 - LinkedList qmin = new LinkedList(); - // 窗口内的最大值的更新结构 - LinkedList qmax = new LinkedList(); - int L = 0; - int R = 0; - // [L..R) -> [0,0) 窗口内无数 [1,1) - // [0,1) -> [0~0] 窗口里只有一个数 - int res = 0; - // L是开头位置,尝试每一个开头 - while (L < arr.length) { - - // 如果此时窗口的开头是L,下面的while工作是:R向右扩到违规为止 - - // R是最后一个达标位置的再下一个,通过下文的break终止 - while (R < arr.length) { - // R位置的数进入窗口后,最小值的更新结构和最大值的更新结构都要更新 - while (!qmin.isEmpty() && arr[qmin.peekLast()] >= arr[R]) { - qmin.pollLast(); - } - qmin.addLast(R); - // R -> arr[R], - while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[R]) { - qmax.pollLast(); - } - qmax.addLast(R); - - // 如果此时不满足了,break。说明窗口已经成长到了第一个不达标的数进来了 - if (arr[qmax.getFirst()] - arr[qmin.getFirst()] > num) { - break; - } - R++; - } - - // R是最后一个达标位置的再下一个,第一个违规的位置 - res += R - L; - - // 检查最小值和最大值的更新结构有没有过期 - if (qmin.peekFirst() == L) { - qmin.pollFirst(); - } - if (qmax.peekFirst() == L) { - qmax.pollFirst(); - } - - // 窗口左边界向右滑动,窗口容量此时减1 - L++; - - } - return res; - } - - // for test - public static int[] getRandomArray(int len) { - if (len < 0) { - return null; - } - int[] arr = new int[len]; - for (int i = 0; i < len; i++) { - arr[i] = (int) (Math.random() * 10); - } - return arr; - } - - // for test - public static void printArray(int[] arr) { - if (arr != null) { - for (int i = 0; i < arr.length; i++) { - System.out.print(arr[i] + " "); - } - System.out.println(); - } - } - - public static void main(String[] args) { - int[] arr = getRandomArray(30); - int num = 5; - printArray(arr); - System.out.println(getNum(arr, num)); - - } - -} - -``` - -> 本题根据窗口滑动建立了单调性,上文的结论 - -### 1.1.6 如何优化一个问题? - -1、 数据状况层面是否可以优化 - -2、 问题本身是否可以优化。单调性,首位指针(头指针往右走,尾指针往左走)等 - -> 遇到一个问题我们先观察,问题本身和范围是否可以建立单调性,至于需要用哪个原型来解决,串口和首位指针法是常见的流程。所以窗口和首位指针主要用来解决单调性的 - -## 1.2 单调栈 - -### 1.2.1 单调栈结构 - -在一个数组中,求每一个位置左边离它最近的比它小的数在哪,右边离它最近的比它小的数在哪。 - -例如[3, 2, 1, 7]。3左边比它小的最近的位置的数的位置没有,为-1,右边是1位置的2。2左边比它小的最近的位置的数的位置没有,为-1,右边是2位置的1等。 - -用一个map来记录,暴力解O(N^2)。单调栈能够在O(N)解决 - - -单调栈算法流程: - -> 草稿纸上模拟这个栈; - -> 没有相等元素的情况:准备一个栈结构,暂定从小到大的单调栈结构。从左往右遍历我们的数组,[3,4,2,5],由于栈空,第一个元素可以进栈,3进栈。 - -> 1位置的数4可以进栈,因为没有破坏从小到大的栈的单调性。 - -> 2位置的2无法直接进栈,因为会破坏栈的单调性。需要弹出栈元素,元素一旦被弹出,生成相应的记录。 - -> 1位置的4弹出,右边最近的比你小的数,就是谁让你弹出的数,所以4的右边最近的比4小的数是2。左边最近比你小的数,就是你在栈中压着的数,所以4的左边最近的比4小的数是3。 - -> 2位置的2此时仍然无法进栈,因为栈中此时还有3,那么3弹出。3的最近的右侧比3小的数是2,3是栈底元素,没有压着的元素,所以3左侧最近的比3小的数没有,位置置为-1。其他元素同理......。 - -> 最后如果没有元素了,栈中元素弹出,此时不是其他元素迫使的弹出,所以自然弹出的右侧最近比它小的无返回-1。左侧最近比它小的看它在栈中是否压着其他元素 - -可以选择任意位置去证明,证明略 - -> 如果存在相等元素的情况,我们栈中每个元素保存为list表示相等元素列表。无法直接进入单调栈时,弹出list最右侧的元素,该元素右侧最近的比自己小的数,就是迫使它弹出的那个数。该元素左侧最近比它小的数,就是自身的这个list压着的的list的最右的数。list的相同元素有两种情况,一种是两个数相等且挨着,另外一种是某个位置释放了中间位置的数后遇到相等元素,进入一个list中去。画栈模拟可看出 - - -```Java - -package class01; - -import java.util.List; -import java.util.ArrayList; -import java.util.Stack; - -public class Code03_MonotonousStack { - - // 数组中没有重复值的情况 - public static int[][] getNearLessNoRepeat(int[] arr) { - int[][] res = new int[arr.length][2]; - Stack stack = new Stack<>(); - for (int i = 0; i < arr.length; i++) { - while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) { - int popIndex = stack.pop(); - int leftLessIndex = stack.isEmpty() ? -1 : stack.peek(); - res[popIndex][0] = leftLessIndex; - res[popIndex][1] = i; - } - stack.push(i); - } - while (!stack.isEmpty()) { - int popIndex = stack.pop(); - int leftLessIndex = stack.isEmpty() ? -1 : stack.peek(); - res[popIndex][0] = leftLessIndex; - res[popIndex][1] = -1; - } - return res; - } - - // arr [3, 2, 1, 4, 5] - // 0 1 2 3 4 - - // 表示 0这个数左边最近比0小的没有,位置是-1,右边1。1位置数左边最近比0小的没有-1,右边2 - // [ - // 0 : [-1, 1 ] - // 1 : [-1, 2 ] - - // ] - // 数组中存在重复值的情况 - public static int[][] getNearLess(int[] arr) { - int[][] res = new int[arr.length][2]; - - - // List -> 放的是位置,同样值的东西,位置压在一起 - // 代表值 底 -> 顶 小 -> 大 - Stack> stack = new Stack<>(); - for (int i = 0; i < arr.length; i++) { // i -> arr[i] 进栈 - // 栈底 -> 栈顶, 小 -> 大 - while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) { - List popIs = stack.pop(); - // 取位于下面位置的列表中,最晚加入的那个 - int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1); - for (Integer popi : popIs) { - res[popi][0] = leftLessIndex; - res[popi][1] = i; - } - } - // 相等的、比你小的 - if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) { - stack.peek().add(Integer.valueOf(i)); - } else { - ArrayList list = new ArrayList<>(); - list.add(i); - stack.push(list); - } - } - while (!stack.isEmpty()) { - List popIs = stack.pop(); - // 取位于下面位置的列表中,最晚加入的那个 - int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1); - for (Integer popi : popIs) { - res[popi][0] = leftLessIndex; - res[popi][1] = -1; - } - } - return res; - } - - // for test - public static int[] getRandomArrayNoRepeat(int size) { - int[] arr = new int[(int) (Math.random() * size) + 1]; - for (int i = 0; i < arr.length; i++) { - arr[i] = i; - } - for (int i = 0; i < arr.length; i++) { - int swapIndex = (int) (Math.random() * arr.length); - int tmp = arr[swapIndex]; - arr[swapIndex] = arr[i]; - arr[i] = tmp; - } - return arr; - } - - // for test - public static int[] getRandomArray(int size, int max) { - int[] arr = new int[(int) (Math.random() * size) + 1]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) (Math.random() * max) - (int) (Math.random() * max); - } - return arr; - } - - // for test - public static int[][] rightWay(int[] arr) { - int[][] res = new int[arr.length][2]; - for (int i = 0; i < arr.length; i++) { - int leftLessIndex = -1; - int rightLessIndex = -1; - int cur = i - 1; - while (cur >= 0) { - if (arr[cur] < arr[i]) { - leftLessIndex = cur; - break; - } - cur--; - } - cur = i + 1; - while (cur < arr.length) { - if (arr[cur] < arr[i]) { - rightLessIndex = cur; - break; - } - cur++; - } - res[i][0] = leftLessIndex; - res[i][1] = rightLessIndex; - } - return res; - } - - // for test - public static boolean isEqual(int[][] res1, int[][] res2) { - if (res1.length != res2.length) { - return false; - } - for (int i = 0; i < res1.length; i++) { - if (res1[i][0] != res2[i][0] || res1[i][1] != res2[i][1]) { - return false; - } - } - - return true; - } - - // 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 size = 10; - int max = 20; - int testTimes = 2000000; - for (int i = 0; i < testTimes; i++) { - int[] arr1 = getRandomArrayNoRepeat(size); - int[] arr2 = getRandomArray(size, max); - if (!isEqual(getNearLessNoRepeat(arr1), rightWay(arr1))) { - System.out.println("Oops!"); - printArray(arr1); - break; - } - if (!isEqual(getNearLess(arr2), rightWay(arr2))) { - System.out.println("Oops!"); - printArray(arr2); - break; - } - } - } -} - -``` - -### 1.2.2 单调栈的应用 - -给定一个只包含正整数的数组arr,arr中任何一个子数组sub,一定都可以算出(sub累加和)*(sub中的最小值)是什么,那么所有子数组中,这个值最大是多少? - -```Java - -package class01; - -import java.util.Stack; - -public class Code04_AllTimesMinToMax { - - public static int max1(int[] arr) { - int max = Integer.MIN_VALUE; - for (int i = 0; i < arr.length; i++) { - for (int j = i; j < arr.length; j++) { - int minNum = Integer.MAX_VALUE; - int sum = 0; - for (int k = i; k <= j; k++) { - sum += arr[k]; - minNum = Math.min(minNum, arr[k]); - } - max = Math.max(max, minNum * sum); - } - } - return max; - } - - public static int max2(int[] arr) { - int size = arr.length; - int[] sums = new int[size]; - sums[0] = arr[0]; - for (int i = 1; i < size; i++) { - sums[i] = sums[i - 1] + arr[i]; - } - int max = Integer.MIN_VALUE; - Stack stack = new Stack(); - for (int i = 0; i < size; i++) { - while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) { - int j = stack.pop(); - max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[j]); - } - stack.push(i); - } - while (!stack.isEmpty()) { - int j = stack.pop(); - max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]); - } - return max; - } - - public static int[] gerenareRondomArray() { - int[] arr = new int[(int) (Math.random() * 20) + 10]; - for (int i = 0; i < arr.length; i++) { - arr[i] = (int) (Math.random() * 101); - } - return arr; - } - - public static void main(String[] args) { - int testTimes = 2000000; - System.out.println("test begin"); - for (int i = 0; i < testTimes; i++) { - int[] arr = gerenareRondomArray(); - if (max1(arr) != max2(arr)) { - System.out.println("FUCK!"); - break; - } - } - System.out.println("test finish"); - } - -} - -``` diff --git "a/14-\343\200\212\350\277\233\351\230\266\343\200\213\347\261\273\344\274\274\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\232\204\351\200\222\345\275\222.md" "b/14-\343\200\212\350\277\233\351\230\266\343\200\213\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\233\270\345\205\263\347\232\204\351\200\222\345\275\222.md" similarity index 59% rename from "14-\343\200\212\350\277\233\351\230\266\343\200\213\347\261\273\344\274\274\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\232\204\351\200\222\345\275\222.md" rename to "14-\343\200\212\350\277\233\351\230\266\343\200\213\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\233\270\345\205\263\347\232\204\351\200\222\345\275\222.md" index 2b9c121..6accfe3 100644 --- "a/14-\343\200\212\350\277\233\351\230\266\343\200\213\347\261\273\344\274\274\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\232\204\351\200\222\345\275\222.md" +++ "b/14-\343\200\212\350\277\233\351\230\266\343\200\213\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\233\270\345\205\263\347\232\204\351\200\222\345\275\222.md" @@ -79,7 +79,7 @@ > 10的75次方推演为:tmp为10开始,对比二进制,需要乘进result。tmp和自己相乘变为10^2,仍然需要,result为1乘10乘10的平方乘10的八次方...。tmp虽然在变化,但我们不是都选择相乘 -==所以上述矩阵次方的乘法,我们可以类似处理。把指数变为二进制。result初始值为单位矩阵,就是对角线全为1的矩阵。其他处理和数字次方的处理类似== +所以上述矩阵次方的乘法,我们可以类似处理。把指数变为二进制。result初始值为单位矩阵,就是对角线全为1的矩阵。其他处理和数字次方的处理类似 **在JDK中,Math.power函数的内部,也是这样实现指数函数的(整数次方)** @@ -89,7 +89,9 @@ 如果某一个递推式,`F(N) = c1F(n-1) + C2F(n-2) + ... + czF(N-k)` k表示最底层减到多少,我们称之为k阶递归式。c1,c2,cz为常数系数,k为常数,那么都有O(logN)的解。系数只会影响到我们的k阶矩阵的不同 -**奶牛问题**:一个农场第一年有一只奶牛A,每一只奶牛每年会产一只小奶牛。假设所有牛都不会死,且小牛需要三年,可以产小奶牛。求N年后牛的数量。 +奶牛问题: + +一个农场第一年有一只奶牛A,每一只奶牛每年会产一只小奶牛。假设所有牛都不会死,且小牛需要三年,可以产小奶牛。求N年后牛的数量。 > 思路:牛的数量的轨迹为:1,2,3,4,6,9...。`F(N) = F(N-1) + F(N-3)` 既今年的牛F(N)等于去年的牛F(N-1) + 三年前牛的数量F(N-3)。三阶问题k=3。 @@ -104,211 +106,196 @@ ``` -```Java +```Go +package main -package class02; +import "fmt" -public class Code01_FibonacciProblem { - // 斐波那契数列暴力解法 - public static int f1(int n) { - if (n < 1) { - return 0; - } - if (n == 1 || n == 2) { - return 1; - } - return f1(n - 1) + f1(n - 2); +// 斐波那契数列暴力解法 +func f1(n int) int { + if n < 1 { + return 0 } - // 线性求解方法 - public static int f2(int n) { - if (n < 1) { - return 0; - } - if (n == 1 || n == 2) { - return 1; - } - int res = 1; - int pre = 1; - int tmp = 0; - for (int i = 3; i <= n; i++) { - tmp = res; - res = res + pre; - pre = tmp; - } - return res; + if n == 1 || n == 2 { + return 1 } - // 矩阵加快速幂O(logN)方法 - public static int f3(int n) { - if (n < 1) { - return 0; - } - if (n == 1 || n == 2) { - return 1; - } - // [ 1 ,1 ] - // [ 1, 0 ] - int[][] base = { - { 1, 1 }, - { 1, 0 } - }; - // 求出base矩阵的n-2次方,得到的矩阵返回 - int[][] res = matrixPower(base, n - 2); - // 最终通过单位矩阵乘以该res,矩阵运算后返回fn的值。 - // 得到xyzk组成的矩阵 - // f(n)F(n-1) = {1, 0} * {x, y} - // {0, 1} {z, k} - // 推导出fn = x + z - return res[0][0] + res[1][0]; + return f1(n-1) + f1(n-2) +} + +// 斐波那契数列线性求解方法 +func f2(n int) int { + if n < 1 { + return 0 } - // 快速求一个矩阵m的p次方 - public static int[][] matrixPower(int[][] m, int p) { - int[][] res = new int[m.length][m[0].length]; - for (int i = 0; i < res.length; i++) { - // 单位矩阵,对角线都是1。相当于矩阵概念中的'1' - res[i][i] = 1; - } - - // res = 矩阵中的1 - int[][] tmp = m;// 矩阵1次方 - // 基于次方的p,做位运算。右移 - for (; p != 0; p >>= 1) { - // 右移之后的末位不是0,才乘当前的tmp - if ((p & 1) != 0) { - res = muliMatrix(res, tmp); - } - // 自己和自己相乘,得到下一个tmp - tmp = muliMatrix(tmp, tmp); - } - return res; + if n == 1 || n == 2 { + return 1 } - // 两个矩阵乘完之后的结果返回 - public static int[][] muliMatrix(int[][] m1, int[][] m2) { - int[][] res = new int[m1.length][m2[0].length]; - for (int i = 0; i < m1.length; i++) { - for (int j = 0; j < m2[0].length; j++) { - for (int k = 0; k < m2.length; k++) { - res[i][j] += m1[i][k] * m2[k][j]; - } - } - } - return res; + res := 1 + pre := 1 + tmp := 0 + + for i := 1; i <= n; i++ { + tmp = res + res = res + pre + pre = tmp } + return res +} - public static int s1(int n) { - if (n < 1) { - return 0; - } - if (n == 1 || n == 2) { - return n; - } - return s1(n - 1) + s1(n - 2); +// 斐波那契矩阵加快速幂O(logN)方法 +func f3(n int) int { + if n < 1 { + return 0 } - public static int s2(int n) { - if (n < 1) { - return 0; - } - if (n == 1 || n == 2) { - return n; - } - int res = 2; - int pre = 1; - int tmp = 0; - for (int i = 3; i <= n; i++) { - tmp = res; - res = res + pre; - pre = tmp; - } - return res; + if n == 1 || n == 2 { + return 1 } - // 奶牛问题O(logN) - public static int s3(int n) { - if (n < 1) { - return 0; - } - if (n == 1 || n == 2) { - return n; - } - int[][] base = { { 1, 1 }, { 1, 0 } }; - int[][] res = matrixPower(base, n - 2); - return 2 * res[0][0] + res[1][0]; + // [ 1 ,1 ] + // [ 1, 0 ] + base := [][]int{ + {1, 1}, + {1, 0}, } + // 求出base矩阵的n-2次方,得到的矩阵返回 + res := matrixPower(base, n-2) + // 最终通过单位矩阵乘以该res,矩阵运算后返回fn的值。 + // 得到xyzk组成的矩阵 + // f(n)F(n-1) = {1, 0} * {x, y} + // {0, 1} {z, k} + // 推导出fn = x + z + return res[0][0] + res[1][0] +} - public static int c1(int n) { - if (n < 1) { - return 0; - } - if (n == 1 || n == 2 || n == 3) { - return n; - } - return c1(n - 1) + c1(n - 3); +// 快速求一个矩阵m的p次方 +func matrixPower(m [][]int, p int) [][]int { + res := make([][]int, len(m)) + for i := 0; i < len(res); i++ { + res[i] = make([]int, len(m[0])) } - public static int c2(int n) { - if (n < 1) { - return 0; - } - if (n == 1 || n == 2 || n == 3) { - return n; - } - int res = 3; - int pre = 2; - int prepre = 1; - int tmp1 = 0; - int tmp2 = 0; - for (int i = 4; i <= n; i++) { - tmp1 = res; - tmp2 = pre; - res = res + prepre; - pre = tmp1; - prepre = tmp2; - } - return res; + for i := 0; i < len(res); i++ { + // 单位矩阵,对角线都是1。相当于矩阵概念中的'1' + res[i][i] = 1 } - public static int c3(int n) { - if (n < 1) { - return 0; + // res = 矩阵中的1 + tmp := m // 矩阵1次方 + // 基于次方的p,做位运算。右移 + for ; p != 0; p >>= 1 { + // 右移之后的末位不是0,才乘当前的tmp + if (p & 1) != 0 { + res = muliMatrix(res, tmp) } - if (n == 1 || n == 2 || n == 3) { - return n; + // 自己和自己相乘,得到下一个tmp + tmp = muliMatrix(tmp, tmp) + } + return res +} + +// 两个矩阵乘完之后的结果返回 +func muliMatrix(m1 [][]int, m2 [][]int) [][]int { + res := make([][]int, len(m1)) + for i := 0; i < len(res); i++ { + res[i] = make([]int, len(m2[0])) + } + + for i := 0; i < len(m1); i++ { + for j := 0; j < len(m2[0]); j++ { + for k := 0; k < len(m2); k++ { + res[i][j] += m1[i][k] * m2[k][j] + } } - // 原始矩阵 - int[][] base = { - { 1, 1, 0 }, - { 0, 0, 1 }, - { 1, 0, 0 } }; - int[][] res = matrixPower(base, n - 3); - return 3 * res[0][0] + 2 * res[1][0] + res[2][0]; + } + return res +} + +// 奶牛问题O(logN) +func s3(n int) int { + if n < 1 { + return 0 + } + + if n == 1 || n == 2 { + return n + } + + base := [][]int{ + {1, 1}, + {1, 0}, } - public static void main(String[] args) { - int n = 19; - System.out.println(f1(n)); - System.out.println(f2(n)); - System.out.println(f3(n)); - System.out.println("==="); + res := matrixPower(base, n-2) + return 2*res[0][0] + res[1][0] +} - System.out.println(s1(n)); - System.out.println(s2(n)); - System.out.println(s3(n)); - System.out.println("==="); +// 奶牛问题暴力递归 +func c1(n int) int { + if n < 1 { + return 0 + } + if n == 1 || n == 2 || n == 3 { + return n + } - System.out.println(c1(n)); - System.out.println(c2(n)); - System.out.println(c3(n)); - System.out.println("==="); + return c1(n-1) + c1(n-3) +} + +// 奶牛问题线性解 +func c2(n int) int { + if n < 1 { + return 0 + } + if n == 1 || n == 2 || n == 3 { + return n + } + res := 3 + pre := 2 + prepre := 1 + tmp1 := 0 + tmp2 := 0 + for i := 4; i <= n; i++ { + tmp1 = res + tmp2 = pre + res = res + prepre + pre = tmp1 + prepre = tmp2 + } + return res +} +// 奶牛问题矩阵解 +func c3(n int) int { + if n < 1 { + return 0 + } + if n == 1 || n == 2 || n == 3 { + return n } + // 原始矩阵 + base := [][]int{ + {1, 1, 0}, + {0, 0, 1}, + {1, 0, 0}, + } + res := matrixPower(base, n - 3) + return 3 * res[0][0] + 2 * res[1][0] + res[2][0] } +func main() { + n := 19 + fmt.Println(f1(n)) + fmt.Println(f2(n)) + fmt.Println(c1(n)) + fmt.Println(c3(n)) +} ``` ## 1.4 迈楼梯问题 @@ -342,8 +329,7 @@ F(N) = F(n-1) + F(n-3) - F(n-10) **十阶递推,求10乘10的原始矩阵,拿到矩阵关系表达式** - -==每年,这种问题在面试笔试中大量出现,兔子生乌龟问题,乌龟生兔子问题,等等,层出不穷,都可以用这种方法模型求解== +每年,这种问题在面试笔试中大量出现,兔子生乌龟问题,乌龟生兔子问题,等等,层出不穷,都可以用这种方法模型求解 ## 1.5 递推经典例题一 @@ -377,4 +363,4 @@ F(N) = F(N-1) + F(N-2) **仍然是一个二阶递推式,菲波那切数列问题** -==菲波那切数列问题及其推广,适用递推的限制为:**严格的,没有条件转移的递推**== +菲波那切数列问题及其推广,适用递推的限制为:**严格的,没有条件转移的递推** 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)) } ``` 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 } ``` 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 } ``` 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" new file mode 100644 index 0000000..b3de81a --- /dev/null +++ "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" @@ -0,0 +1,433 @@ +[TOC] + +# 1 线段树(又名为线段修改树) + +线段树所要解决的问题是,区间的修改,查询和更新,如何更新查询的更快? + +线段树结构提供三个主要的方法, 假设大小为N的数组,以下三个方法,均要达到O(logN) : + +```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 +} +``` + + +## 1.1 线段树概念建立 + +### 1.1.1 累加和数组建立 + +1、对于大小为n的数组,我们二分它,每次二分我们都记录一个信息 + +2、对于每次二分,成立树结构,我们想拿任何区间的信息,可以由我们的二分结构组合得到。例如我们1到8的数组,可以二分得到的信息为: + +``` +graph TD +'1-8'-->'1-4' +'1-8'-->'5-8' +'1-4'-->'1-2' +'1-4'-->'3-4' +'5-8'-->'5-6' +'5-8'-->'7-8' +'1-2'-->'1' +'1-2'-->'2' +'3-4'-->'3' +'3-4'-->'4' +'5-6'-->'5' +'5-6'-->'6' +'7-8'-->'7' +'7-8'-->'8' +``` + +每一个节点的信息,可以由该节点左右孩子信息得到,最下层信息就是自己的信息。由以上的规则,对于N个数,我们需要申请2N-1个空间用来保存节点信息。如果N并非等于2的某次方,我们把N补成2的某次方的长度,用来保证我们构建出来的信息数是满二叉树。例如我们的长度是6,我们补到8个,后两个位置值为0。 + + +对于任意的N,我们需要准备多少空间,可以把N补成2的某次方,得到的二分信息都装下?答案是4N。4N虽然有可能多分空间,但是多余的空间都是0,并无影响,而且兼容N为任意值的情况 + +例如四个数长度的数组arr[4]{3,2,5,7},我们得到累加和的二分信息为如下的树: + +``` +graph TD +'1到4=17'-->'1到2=5' +'1到4=17'-->'3到4=12' +'1到2=5'-->'3' +'1到2=5'-->'2' +'3到4=12'-->'5' +'3到4=12'-->'7' +``` + +我们申请4N的空间,即16,arr[16]。0位置不用。arr[1]=17,arr[2]=5,arr[3]=12,arr[4]=3,arr[5]=2,arr[6]=5,arr[7]=7。剩下位置都为0。任何一个节点左孩子下标为2i,右孩子下标为2i+1 + + +得到累加和信息的分布树的大小,和值的情况,那么update更新树,和add累加树,同样的大小和同样的坐标关系构建。 + + +### 1.1.2更新结构数组建立 + +懒更新概念,例如有8个数,我们要把1到6的数都减小2。那么先看1到6是否完全囊括8个数,如果囊括直接更新。很显然这里没有囊括,记录要更新1到6,下发该任务给1到4和5到8。1到6完全囊括1到4,记录到lazy中,不再下发;5到8没有囊括1到6,继续下发给5到6和7到8,5到6被囊括,记录到lazy不再继续下发,7到8不接受该任务 + +这种懒更新机制的时间复杂度为O(logN),由于一个区间经过左右子树下发,只会经过一个绝对路径到叶子节点,其他节点都会被懒住。如果某个节点有新的任务进来,会把之前懒住的信息下发给左右孩子 + + +对于update操作,如果update操作经过的信息节点上存在懒任务,那么该次update操作会取消该节点的lazy,无需下发,因为下发了也会给update覆盖掉; + + +```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] + } + + // 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 + } +} + +// 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) +} + +// 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) +} + +// 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)) +} +``` + +## 1.2 线段树案例实战 + +想象一下标准的俄罗斯方块游戏,X轴是积木最终下落到底的轴线 +下面是这个游戏的简化版: + +1)只会下落正方形积木 + +2)[a,b] -> 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落 + +3)认为整个X轴都可能接住积木,也就是说简化版游戏是没有整体的左右边界的 + +4)没有整体的左右边界,所以简化版游戏不会消除积木,因为不会有哪一层被填满。 + +给定一个N*2的二维数组matrix,可以代表N个积木依次掉落, +返回每一次掉落之后的最大高度 + +> 线段树原结构,是收集范围累加和,本题是范围上收集最大高度当成收集的信息 + +```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做离散化,避免多申请空间 +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] = "" + } + + m := make(map[int]int, 0) + count := 0 + for key := range pos { + count++ + m[key] = count + } + return m +} +``` + +本题为leetCode原题:https://leetcode.com/problems/falling-squares/ + + +## 1.3 什么样的题目可以用线段树来解决? + +区间范围上,统一增加,或者统一更新一个值。大范围信息可以只由左、右两侧信息加工出, +而不必遍历左右两个子范围的具体状况 + 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.md" "b/18-\343\200\212\350\277\233\351\230\266\343\200\213\347\272\277\346\256\265\346\240\221.md" deleted file mode 100644 index 2876d72..0000000 --- "a/18-\343\200\212\350\277\233\351\230\266\343\200\213\347\272\277\346\256\265\346\240\221.md" +++ /dev/null @@ -1,511 +0,0 @@ -[TOC] - -# 1 线段树(又名为线段修改树) - -线段树所要解决的问题是,区间的修改,查询和更新,如何更新查询的更快? - -线段树结构提供三个主要的方法, 假设大小为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); - -``` - - -## 1.1 线段树概念建立 - -### 1.1.1 累加和数组建立 - -1、对于大小为n的数组,我们二分它,每次二分我们都记录一个信息 - -2、对于每次二分,成立树结构,我们想拿任何区间的信息,可以由我们的二分结构组合得到。例如我们1到8的数组,可以二分得到的信息为: - -``` -graph TD -'1-8'-->'1-4' -'1-8'-->'5-8' -'1-4'-->'1-2' -'1-4'-->'3-4' -'5-8'-->'5-6' -'5-8'-->'7-8' -'1-2'-->'1' -'1-2'-->'2' -'3-4'-->'3' -'3-4'-->'4' -'5-6'-->'5' -'5-6'-->'6' -'7-8'-->'7' -'7-8'-->'8' -``` - -每一个节点的信息,可以由该节点左右孩子信息得到,最下层信息就是自己的信息。由以上的规则,对于N个数,我们需要申请2N-1个空间用来保存节点信息。如果N并非等于2的某次方,我们把N补成2的某次方的长度,用来保证我们构建出来的信息数是满二叉树。例如我们的长度是6,我们补到8个,后两个位置值为0。 - - -对于任意的N,我们需要准备多少空间,可以把N补成2的某次方,得到的二分信息都装下?答案是4N。4N虽然有可能多分空间,但是多余的空间都是0,并无影响,而且兼容N为任意值的情况 - -例如四个数长度的数组arr[4]{3,2,5,7},我们得到累加和的二分信息为如下的树: - -``` -graph TD -'1到4=17'-->'1到2=5' -'1到4=17'-->'3到4=12' -'1到2=5'-->'3' -'1到2=5'-->'2' -'3到4=12'-->'5' -'3到4=12'-->'7' -``` - -我们申请4N的空间,即16,arr[16]。0位置不用。arr[1]=17,arr[2]=5,arr[3]=12,arr[4]=3,arr[5]=2,arr[6]=5,arr[7]=7。剩下位置都为0。任何一个节点左孩子下标为2i,右孩子下标为2i+1 - - -得到累加和信息的分布树的大小,和值的情况,那么update更新树,和add累加树,同样的大小和同样的坐标关系构建。 - - -### 1.1.2更新结构数组建立 - -懒更新概念,例如有8个数,我们要把1到6的数都减小2。那么先看1到6是否完全囊括8个数,如果囊括直接更新。很显然这里没有囊括,记录要更新1到6,下发该任务给1到4和5到8。1到6完全囊括1到4,记录到lazy中,不再下发;5到8没有囊括1到6,继续下发给5到6和7到8,5到6被囊括,记录到lazy不再继续下发,7到8不接受该任务 - -这种懒更新机制的时间复杂度为O(logN),由于一个区间经过左右子树下发,只会经过一个绝对路径到叶子节点,其他节点都会被懒住。如果某个节点有新的任务进来,会把之前懒住的信息下发给左右孩子 - - -对于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; - } - - } - - // 暴力解法,用来做对数器 - 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; - } - - } - - 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; - } - - 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; - } - - 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() ? "通过" : "未通过")); - - } - -} -``` - -## 1.2 线段树案例实战 - -想象一下标准的俄罗斯方块游戏,X轴是积木最终下落到底的轴线 -下面是这个游戏的简化版: - -1)只会下落正方形积木 - -2)[a,b] -> 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落 - -3)认为整个X轴都可能接住积木,也就是说简化版游戏是没有整体的左右边界的 - -4)没有整体的左右边界,所以简化版游戏不会消除积木,因为不会有哪一层被填满。 - -给定一个N*2的二维数组matrix,可以代表N个积木依次掉落, -返回每一次掉落之后的最大高度 - -> 线段树原结构,是收集范围累加和,本题是范围上收集最大高度当成收集的信息 - -```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); - } - - } - - // 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; - } - - 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; - } - -} -``` - -本题为leetCode原题:https://leetcode.com/problems/falling-squares/ - - -## 1.3 什么样的题目可以用线段树来解决? - -区间范围上,统一增加,或者统一更新一个值。大范围信息可以只由左、右两侧信息加工出, -而不必遍历左右两个子范围的具体状况 - 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" new file mode 100644 index 0000000..36456fd --- /dev/null +++ "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" @@ -0,0 +1,540 @@ +[TOC] + +# 1 打表技巧和矩阵处理技巧 + +在一个数组arr中,每个数的大小不超过1000,例如[10,9,6,12],所有的数,求所有数质数因子的个数总和? + +`10=2*5` + +`9=3*3` + +`6=3*3` + +`12=3*2*2` + +我们可以把1000以内的数的质数因子个数求出来,存到我们的表中,查表即可 + + +## 1.1 打表法 + +1)问题如果返回值不太多,可以用hardcode的方式列出,作为程序的一部分 + +2)一个大问题解决时底层频繁使用规模不大的小问题的解,如果小问题的返回值满足条件1),可以把小问题的解列成一张表,作为程序的一部分 + +3)打表找规律(本节课重点) + + +### 1.1.1 打表找规律 + +1)某个面试题,输入参数类型简单,并且只有一个实际参数 + +2)要求的返回值类型也简单,并且只有一个 + +3)用暴力方法,把输入参数对应的返回值,打印出来看看,进而优化code + + +### 1.1.2 例题1 小虎买苹果 + +小虎去买苹果,商店只提供两种类型的塑料袋,每种类型都有任意数量。 + +1)能装下6个苹果的袋子 + +2)能装下8个苹果的袋子 + +小虎可以自由使用两种袋子来装苹果,但是小虎有强迫症,他要求自己使用的袋子数量必须最少,且使用的每个袋子必须装满。 +给定一个正整数N,返回至少使用多少袋子。如果N无法让使用的每个袋子必须装满,返回-1 + + +> 暴力思路,例如N=100个苹果,我们全部用8号袋装,最多使用12个8号袋子,剩4个苹果,6号袋没装满。8号袋减1,需要2个6号袋,满足。如果依次递减8号袋,为0个仍未有答案,则无解 + + +```Go +package main + +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 + } + + 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 + } +} + +// 根据打表规律写code +func minBagAwesome(apple int) int { + if apple & 1 != 0 {// 如果是奇数,返回-1 + return -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 +} + +// 打表看规律,摒弃数学规律 +func main() { + for apple:=1; apple < 100; apple++ { + fmt.Println(minBags(apple)) + } +} +``` + +### 1.1.2 例题2 牛羊吃草 + +给定一个正整数N,表示有N份青草统一堆放在仓库里 +有一只牛和一只羊,牛先吃,羊后吃,它俩轮流吃草 +不管是牛还是羊,每一轮能吃的草量必须是: + +1,4,16,64…(4的某次方) + +谁最先把草吃完,谁获胜 + +假设牛和羊都绝顶聪明,都想赢,都会做出理性的决定 + +根据唯一的参数N,返回谁会赢 + + +> 暴力思路打表找规律 + +``` +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 "先手" + } + } + + // 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 "后手" +} + +// 根据打表的规律,写代码 +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))) + } +} +``` + +### 1.1.3 例题3 + +定义一种数:可以表示成若干(数量>1)连续正数和的数 +比如: + +5 = 2+3,5就是这样的数 + +12 = 3+4+5,12就是这样的数 + +1不是这样的数,因为要求数量大于1个、连续正数和 + +2 = 1 + 1,2也不是,因为等号右边不是连续正数 + +给定一个参数N,返回是不是可以表示成若干连续正数和的数 + + +```Go +package main + +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 +} + +// 根据打表的规律写代码 +func isMSum2(num int) bool { + if num < 3 { + return false + } + return (num & (num - 1)) != 0 +} + +// 打表 +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!") + } + } + fmt.Println("test end") +} +``` + +### 1.2 矩阵处理技巧 + +1)zigzag打印矩阵 + +2)转圈打印矩阵 + +3)原地旋转正方形矩阵 + +核心技巧:找到coding上的宏观调度 + +#### zigzag打印矩阵 + +> 矩阵的特殊轨迹问题,不要把思维限制在具体某个坐标怎么变化 + +对于一个矩阵,如何绕圈打印,例如: + +```math +\begin{matrix} +1&2&3 \\ +4&5&6 \\ +7&8&9 \\ +\end{matrix} +``` + +打印的顺序为:1,2,4,7,5,3,6,8,9 + +> 思路:准备A和B两个点,坐标都指向0,0位置。A和B同时走,A往右走,走到尽头后往下走,B往下走,走到不能再走了往右走。通过这么处理,A和B每个位置的连线都是一条斜线,且无重复。A和B每同时走一步,打印每次逆序打印,即开始时从B往A打印,下一步从A往B打印,循环往复 + + +```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 + } + if dR == endR { + dC = dC + 1 + } else { + dR = dR + 1 + } + // A和B来到下一个位置之后,改变打印方向 + fromUp = !fromUp + } + fmt.Println() +} + +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) +} +``` + +#### 转圈打印矩阵 + + +```math +\begin{matrix} +1&2&3&4 \\ +5&6&7&8 \\ +9&10&11&12 \\ +13&14&15&16 \\ +\end{matrix} +``` + +打印轨迹是:1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10 + +> 思路:每个圈,我们知道左上角的位置,和右下角的位置,我们就可以得到需要转圈的圈的大小, + +```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-- + } +} + +// 当前打印,左上角和右下角的位置 +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-- + } + } +} + +// 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) +} +``` + +#### 矩阵调整-原地旋转正方形矩阵 + +必须要是正方形矩阵,非正方形的旋转会越界;题意的意思是每一个数都顺时针旋转90度 + + +```math +\begin{matrix} +1&2&3&4 \\ +5&6&7&8 \\ +9&10&11&12 \\ +13&14&15&16 \\ +\end{matrix} +``` + +调整后的结构为: + +```math +\begin{matrix} +13&9&5&1 \\ +14&10&6&2 \\ +5&11&7&3 \\ +16&12&8&4 \\ +\end{matrix} +``` + + +> 思路:一圈一圈的转,和旋转打印思路比较像。按圈,再按小组旋转,第一圈的第一个小组为四个角。分别为:1,4,16,13;第二小组为:2,8,15,9;依次旋转小组,最终达到旋转该圈的目的。接着旋转下一个圈的各个小组。每一层的小组数目等于该圈的边长减1 + +```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-- + } +} + +// 当前需要转的圈的左上角和右下角 +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 + } +} + +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() + } +} + +//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) +} +``` + +> 大量的矩阵变换都会涉及到一个宏观调度,不到万不得已,不要把自己陷入每个位置怎么变,扣每个位置的变化,会非常难 + + + + diff --git "a/19-\343\200\212\350\277\233\351\230\266\343\200\213\346\211\223\350\241\250\346\212\200\345\267\247\345\222\214\347\237\251\351\230\265\345\244\204\347\220\206\346\212\200\345\267\247.md" "b/19-\343\200\212\350\277\233\351\230\266\343\200\213\346\211\223\350\241\250\346\212\200\345\267\247\345\222\214\347\237\251\351\230\265\345\244\204\347\220\206\346\212\200\345\267\247.md" deleted file mode 100644 index 6c173db..0000000 --- "a/19-\343\200\212\350\277\233\351\230\266\343\200\213\346\211\223\350\241\250\346\212\200\345\267\247\345\222\214\347\237\251\351\230\265\345\244\204\347\220\206\346\212\200\345\267\247.md" +++ /dev/null @@ -1,475 +0,0 @@ -[TOC] - -# 1 打表技巧和矩阵处理技巧 - -在一个数组arr[]中,每个数的大小不超过1000,例如[10,9,6,12],所有的数,求所有数质数因子的个数总和? - -`10=2*5` - -`9=3*3` - -`6=3*3` - -`12=3*2*2` - -我们可以把1000以内的数的质数因子个数求出来,存到我们的表中,查表即可 - - -## 1.1 打表法 - -1)问题如果返回值不太多,可以用hardcode的方式列出,作为程序的一部分 - -2)一个大问题解决时底层频繁使用规模不大的小问题的解,如果小问题的返回值满足条件1),可以把小问题的解列成一张表,作为程序的一部分 - -3)打表找规律(本节课重点),有关1)和2)内容欢迎关注后序课程 - - - -### 1.1.1 打表找规律 - -1)某个面试题,输入参数类型简单,并且只有一个实际参数 - -2)要求的返回值类型也简单,并且只有一个 - -3)用暴力方法,把输入参数对应的返回值,打印出来看看,进而优化code - - -### 1.1.2 例题1 小虎买苹果 - -小虎去买苹果,商店只提供两种类型的塑料袋,每种类型都有任意数量。 - -1)能装下6个苹果的袋子 - -2)能装下8个苹果的袋子 - -小虎可以自由使用两种袋子来装苹果,但是小虎有强迫症,他要求自己使用的袋子数量必须最少,且使用的每个袋子必须装满。 -给定一个正整数N,返回至少使用多少袋子。如果N无法让使用的每个袋子必须装满,返回-1 - - -> 暴力思路,例如N=100个苹果,我们全部用8号袋装,最多使用12个8号袋子,剩4个苹果,6号袋没装满。8号袋减1,需要2个6号袋,满足。如果依次递减8号袋,为0个仍未有答案,则无解 - - -```Java -public class Code01_AppleMinBags { - - 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); - } - return bag6 == -1 ? -1 : bag6 + bag8; - } - - // 如果剩余苹果rest可以被装6个苹果的袋子搞定,返回袋子数量 - // 不能搞定返回-1 - public static int minBagBase6(int rest) { - return rest % 6 == 0 ? (rest / 6) : -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; - } - return (apple - 18) / 8 + 3; - } - - // 打表看规律,摒弃数学规律 - public static void main(String[] args) { - for(int apple = 1; apple < 100;apple++) { - System.out.println(apple + " : "+ minBags(apple)); - } - - } - -} -``` - -### 1.1.2 例题2 牛羊吃草 - -给定一个正整数N,表示有N份青草统一堆放在仓库里 -有一只牛和一只羊,牛先吃,羊后吃,它俩轮流吃草 -不管是牛还是羊,每一轮能吃的草量必须是: - -1,4,16,64…(4的某次方) - -谁最先把草吃完,谁获胜 - -假设牛和羊都绝顶聪明,都想赢,都会做出理性的决定 - -根据唯一的参数N,返回谁会赢 - - -> 暴力思路打表找规律 - -```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; - } - return "后手"; - } - - // 根据打表的规律,写代码 - public static String winner2(int n) { - if (n % 5 == 0 || n % 5 == 2) { - return "后手"; - } else { - return "先手"; - } - } - - // 暴力打表找规律 - public static void main(String[] args) { - for (int i = 0; i <= 50; i++) { - System.out.println(i + " : " + winner1(i)); - } - } - -} -``` - -### 1.1.3 例题3 - -定义一种数:可以表示成若干(数量>1)连续正数和的数 -比如: - -5 = 2+3,5就是这样的数 - -12 = 3+4+5,12就是这样的数 - -1不是这样的数,因为要求数量大于1个、连续正数和 - -2 = 1 + 1,2也不是,因为等号右边不是连续正数 - -给定一个参数N,返回是不是可以表示成若干连续正数和的数 - - -```Java -public class Code03_MSumToN { - - // 暴力法 - 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; - } - } - return false; - } - - // 根据打表的规律写代码 - public static boolean isMSum2(int num) { - 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!"); - } - } - System.out.println("test end"); - - } -} -``` - -### 1.2 矩阵处理技巧 - -1)zigzag打印矩阵 - -2)转圈打印矩阵 - -3)原地旋转正方形矩阵 - -核心技巧:找到coding上的宏观调度 - -#### zigzag打印矩阵 - -> 矩阵的特殊轨迹问题,不要把思维限制在具体某个坐标怎么变化 - -对于一个矩阵,如何绕圈打印,例如: - -```math -\begin{matrix} -1&2&3 \\ -4&5&6 \\ -7&8&9 \\ -\end{matrix} -``` - -打印的顺序为:1,2,4,7,5,3,6,8,9 - -> 思路:准备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; - } - 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--] + " "); - } - } else { - while (dR != tR - 1) { - System.out.print(m[dR--][dC++] + " "); - } - } - } - - public static void main(String[] args) { - int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }; - printMatrixZigZag(matrix); - - } - -} - -``` - -#### 转圈打印矩阵 - - -```math -\begin{matrix} -1&2&3&4 \\ -5&6&7&8 \\ -9&10&11&12 \\ -13&14&15&16 \\ -\end{matrix} -``` - -打印轨迹是:1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10 - -> 思路:每个圈,我们知道左上角的位置,和右下角的位置,我们就可以得到需要转圈的圈的大小, - -```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--); - } - } - - // 当前打印,左上角和右下角的位置 - 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--; - } - } - } - - 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); - - } - -} -``` - -#### 矩阵调整-原地旋转正方形矩阵 - -必须要是正方形矩阵,非正方形的旋转会越界;题意的意思是每一个数都顺时针旋转90度 - - -```math -\begin{matrix} -1&2&3&4 \\ -5&6&7&8 \\ -9&10&11&12 \\ -13&14&15&16 \\ -\end{matrix} -``` - -调整后的结构为: - -```math -\begin{matrix} -13&9&5&1 \\ -14&10&6&2 \\ -5&11&7&3 \\ -16&12&8&4 \\ -\end{matrix} -``` - - -> 思路:一圈一圈的转,和旋转打印思路比较像。按圈,再按小组旋转,第一圈的第一个小组为四个角。分别为: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--); - } - } - - // 当前需要转的圈的左上角和右下角 - 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; - } - } - - 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(); - } - } - - 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); - - } - -} -``` - -> 大量的矩阵变换都会涉及到一个宏观调度,不到万不得已,不要把自己陷入每个位置怎么变,扣每个位置的变化,会非常难 - - - - 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)) } ``` 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\346\234\211\345\205\263\347\232\204\347\273\223\346\236\204\345\222\214\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\346\234\211\345\205\263\347\232\204\347\273\223\346\236\204\345\222\214\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\346\234\211\345\205\263\347\232\204\347\273\223\346\236\204\345\222\214\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) } ``` diff --git "a/22-\343\200\212\350\277\233\351\230\266\343\200\213\345\246\202\344\275\225\350\247\243\345\206\263\350\265\204\346\272\220\351\231\220\345\210\266\347\261\273\351\242\230\347\233\256.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" similarity index 98% rename from "22-\343\200\212\350\277\233\351\230\266\343\200\213\345\246\202\344\275\225\350\247\243\345\206\263\350\265\204\346\272\220\351\231\220\345\210\266\347\261\273\351\242\230\347\233\256.md" rename to "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\345\246\202\344\275\225\350\247\243\345\206\263\350\265\204\346\272\220\351\231\220\345\210\266\347\261\273\351\242\230\347\233\256.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可以拿下,对应上述第五点 - 第二问和第三问题解 利用分段统计 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" new file mode 100644 index 0000000..af43197 --- /dev/null +++ "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" @@ -0,0 +1,1445 @@ +[TOC] + +# 1 有序表原理及扩展 + +## 1.1 搜索二叉树 + +经典的搜索二叉树,是没有重复值的,任何节点为头的数,左孩子都比自己小,右孩子都比自己大 + +允许重复值的改进的搜索二叉树,可以在每个节点上增加一个统计词频的数据项。表示出现了几次;但是不可相等的放到左右孩子上,搜索二叉树变平衡时,会影响后续的旋转 + +1、搜索二叉树一定要说明以什么标准来排序 + +2、经典的搜索二叉树,树上没有重复的用来排序的key值 + +3、如果有重复节点的需求,可以在一个节点内部增加数据项 + +## 1.2 搜索二叉树的增删改查 + +### 1.2.1 搜索二叉树的查找和添加 + +- 查找 + +搜索二叉树查询key(查询某个key存在还是不存在),当前节点比自己小,到右子树上去找,当前节点比自己大,到其左孩子上去找,越界,说明不存在 + +1、如果当前节点的value==key,返回true + +2、如果当前节点的valuekey,当前节点向右移动 + +4、如果当前节点变成null,返回false + + +- 添加 + +和查询过程一样,但当前节点滑到空的时候,就插入在这里 + + +- 删除 + +1、先找到key所在的节点 + +2、如果该节点没有左孩子、没有右孩子,直接删除即可(好理解) + +3、如果该节点有左孩子、没有右孩子,直接用左孩子顶替该节点(好理解) + +4、如果该节点没有左孩子、有右孩子,直接用右孩子顶替该节点(好理解) + +5、如果该节点有左孩子、有右孩子,用该节点后继节点顶替该节点(需要旋转调整,没法用左右孩子去替换,原因是左右孩子也有左右孩子) + +> 一个节点的后继节点,就是该节点右孩子的最左的那个节点。 + +``` +graph TD +2-->1 +2-->5 +5-->3 +5-->10 +10-->8 +10-->13 +8-->6 +6-->7 +``` + +比如我要删除5节点,那么5节点的后继节点就是其右子树的最左的孩子,也就是6。把6替换掉5,6的右孩子给它父亲作为左孩子,得到 + +``` +graph TD +2-->1 +2-->6 +6-->3 +6-->10 +10-->8 +10-->13 +8-->7 +``` + +```Go +package main + +import "fmt" + +type Node struct { + Value int + Left *Node // 左孩子指针 + Right *Node // 右孩子指针 + Parent *Node // 指向父亲的指针 +} + +// 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, + } +} + +// 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 node +} + +// Insert 向二叉搜索树中插入一个节点 +func (tree *BinarySearchTree) Insert(element int) *Node { + // 首先如果这个树是空的,把该节点当成头节点 + if tree.Root == nil { + tree.Root = createNode(element, nil, nil, nil) + tree.Size++ + return tree.Root + } + + // 需要插入在该节点下面。经过上面的base case,这里tree.root一定不为nil + searchTempNode := tree.Root + insertParentNode := searchTempNode + + for searchTempNode != nil { + insertParentNode = searchTempNode + if element < searchTempNode.Value { + searchTempNode = searchTempNode.Left + } else { + searchTempNode = searchTempNode.Right + } + } + + newNode := createNode(element, insertParentNode, nil, nil) + if insertParentNode.Value > newNode.Value { + insertParentNode.Left = newNode + } else { + insertParentNode.Right = newNode + } + tree.Size++ + return newNode +} + +// delete 删除二叉搜索树中某个值的对应的节点。删除节点,每个节点由于加入向上的指针,那么旋转的时候会方便些 +func (tree *BinarySearchTree) delete(element int) *Node { + deleteNode := tree.Search(element) + if deleteNode != nil { + return tree.deleteByNode(deleteNode) + } else { + return nil + } +} + +// 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 +} + +// 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 + } + + if newNode != nil { + newNode.Parent = nodeToReplace.Parent + } + return newNode +} + +// getMinimumValue 查找二叉搜索树中最小的值 +func (tree *BinarySearchTree) getMinimumValue() int { + return tree.getMinimumNode(tree.Root).Value +} + +// getMinimumNode 查找二叉搜索树中最小值所在的节点 +func (tree *BinarySearchTree) getMinimumNode(node *Node) *Node { + for node.Left != nil { + node = node.Left + } + return node +} + +func (tree *BinarySearchTree) getMaximumValue() int { + return tree.getMaximumNode(tree.Root).Value +} + +func (tree *BinarySearchTree) getMaximumNode(node *Node) *Node { + for node.Right != nil { + node = node.Right + } + return node +} + +// contains 判断二叉搜索树中存不存在element +func (tree *BinarySearchTree) contains(element int) bool { + return tree.Search(element) != nil +} + +// 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 + } +} + +// printTreeInOrder 中序遍历 +func (tree *BinarySearchTree) printTreeInOrder() { + printInOrder(tree.Root) +} + +// printTreePreOrder 先序遍历 +func (tree *BinarySearchTree) printTreePreOrder() { + printPreOrder(tree.Root) +} + +// printTreePostOrder 后序遍历 +func (tree *BinarySearchTree) printTreePostOrder() { + printPostOrder(tree.Root) +} + +func printInOrder(head *Node) { + if head != nil { + printInOrder(head.Left) + fmt.Println(head.Value) + printInOrder(head.Right) + } +} + +func printPreOrder(head *Node) { + if head != nil { + fmt.Println(head.Value) + printInOrder(head.Left) + printInOrder(head.Right) + } +} + +func printPostOrder(head *Node) { + if head != nil { + printInOrder(head.Left) + printInOrder(head.Right) + fmt.Println(head.Value) + } +} +``` + + +## 1.3 传统搜索二叉树存在的问题 + + +1)基础的搜索二叉树,添加、删除时候不照顾平衡性 + +2)数据状况很差时,性能就很差 + + +> 给搜索二叉树引入两个动作:左旋、右旋 + + +输入状况,决定性能。比如输入状况构建出来的树,严重不平衡。极端情况是只有一条通往底部的路径,高度为n; + +平衡二叉树的定义,任何子树,左子树的高度和右子树的高度差,不大于1。所以对于n个节点,平衡二叉树的高度,就是logN + + +### 1.3.1 平衡搜索二叉树 + + +平衡搜索二叉树,就是在插入和删除的过程中,动态的保持平衡性。保持平衡的代价保持在logN。平衡搜索二叉树的实现有很多,红黑树只是其中一种 + + +### 1.3.2 左旋和右旋 + +左旋和右旋针对头结点而言的,即对哪个头结点进行左旋还是右旋;顾名思义如果对头结点为a的子树右旋,那么a倒到右边,a的左孩子顶上来,到a原来的位置上去。a原来左孩子的右孩子,现在当做a的左孩子,如下图 + + +``` +graph TD +a-->b +a-->c +c-->d +c-->e +b-->f +b-->g +``` + +a右旋,得到: + + +``` +graph TD +b-->f +b-->a +a-->g +a-->c +c-->d +c-->e +``` + +同理,a左旋,得到: + +``` +graph TD +c-->a +c-->e +a-->b +a-->d +b-->f +b-->g + +``` + + +带左旋和右旋的搜索二叉树,在经典搜索二叉树上做的扩展,继承经典搜索二叉树。 + +```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 +} +``` + + + +## 1.4 有序表 + + +在Java中,就是TreeMap,有序表和Hash表(HashMap)的操作类似,但是有序表中自动把我们的key排好了顺序,我们也可以传入比较器,自定义对比和排序的规则;我们可以通过TreeMap直接得到最大节点和最小节点,也可以获取大于某个值的第一个Key是什么等等 + + +为什么TreeMap可以做到有序,原因是TreeMap的底层是一个平衡搜索二叉树 + + +- Hash表和有序表对比 + +1、HashMap的所有操作是O(1)的,TreeMap的所有操作是O(logN) + +2、使用有序表的Key必须是可以比较的,没法自然比较的需要传入自定义比较器 + + +## 1.5 有序表的实现(AVL树,SB树,红黑树) + +在有序表中,有序表是一种规范,类似于接口名。规范为key要按序组织,所有操作要是O(logN)等。各种树结构可以实现有序表的功能。其中红黑树只是有序表的一个实现 + + +**AVL树,SB树,红黑树,都是有序表的一种实现。都是平衡搜索二叉树,这三种树在功能和时间复杂度上几乎无差别,在实现细节上也就是在常数时间上,会有差别。三种树调整检查哪些节点的平衡性相同,下文进行说明。每种树针对每个节点的平衡性调整不同,但是都是使用左旋和右旋两个动作** + +- AVL树,SB树,红黑树的共性 + +1)都是搜索二叉树 + +2)插入、删除、查询(一切查询)搜索二叉树怎么做,这些结构都这么做 + +3)使用调整的基本动作都只有左旋、右旋 + +4)插入、删除时,从最底层被影响到的节点开始,对往上路径的节点做平衡性检查 + +5)因为只对一条向上路径的每个节点做O(1)的检查和调整,所以可以做到O(logN) + + +- AVL树,SB树,红黑树不同之处 + +1)平衡性的约束不同 + +AVL树最严格、SB树稍宽松、红黑树最宽松 + +2)插入、删除和搜索二叉树一样,但是额外,做各自的平衡性调整。各自的平衡性调整所使用的动作都是左旋或者右旋 + +### 1.5.1 AVL树 + +是这三个平衡搜索二叉树中,平衡性最严格的,左树高度和右树高度的绝对值,严格小于2 + +AVL树在插入节点的时候,只会向上检查节点的平衡性有没有被破坏。删除节点也一样,只会检查删除的那个节点向上的那条链上的节点有没被破坏。删除的时候,如果被删除的节点没有左右孩子那么直接检查,如果有右孩子,是去检查后继节点原来所在的位置的向上节点的平衡性 + +**实质上三种树删除和插入节点,检查哪些节点需要调整平衡都是这样的查找规则,对于删除来说,只有左树和只有右树没影响,如果左右树都存在,是去检查后继节点原来所在的位置向上的平衡性。只是具体到某个节点平衡性的处理上,三种树不一样** + + +#### 1.5.1.1 AVL树针对某个节点的平衡性处理 + +1、该节点左树高度和右树高度差的绝对值|L-R|,小于2。不违规,无须调整 + +2、|L-R|大于1,说明要不然左树高度大了,要不然右树高度大了。而且之前的每一步都进行了调整,所以高度差不会超过2。高度不平衡对应四种情况,被称为RR形违规,RL形违规,LL形违规,LR形违规。首字母表示左树变高了还是右树变高了,比如RR和RL都表示经过插入或者删除,右树的高度变大了 + + +RR表示,右子节点的右树过长,RL表示右子节点的左树过长。同理LL表示左子节点的左子树高度过长导致,LR表示左子节点的右树高度过长导致的不平衡 + +- LL形违规,对该节点进行一次右旋即可 + +``` +graph TD +x-->y +x-->p +y-->z +y-->t +z-->k +``` + +右旋后得到: + +``` +graph TD +y-->z +y-->x +z-->k +x-->t +x-->p +``` + + +- 同理RR形违规,对该节点进行一次左旋即可 + + +- LR形,让底部的孙子节点,来到顶部来。 + +下面例子,想办法让x的孙子节点t节点来到顶部,先对y节点进行一次左旋,再对x节点做一次右旋 + +``` +graph TD +x-->y +x-->p +y-->z +y-->t +t-->k +``` + +先对y节点进行一次左旋 + +``` +graph TD +x-->t +x-->p +y-->z +t-->y +t-->k +``` + +再对x节点做一次右旋 + +``` +graph TD +t-->y +t-->x +x-->k +x-->p +y-->z +``` + +原x的孙子节点t此时,调整到的顶部 + +- 同理RL形,也是让超过长度的那一侧底部的孙子节点,来到顶部来。 + + +**LL形和RR形旋转一次O(1),LR和RL形旋转两次,也是O(1)。那么即使删除或者添加的节点影响的整个向上的链路,整体复杂度也是O(logN)** + + +AVL树继承自带左右旋的平衡搜索二叉树,在此基础上做的扩展,重写继承的抽象类平衡二叉搜索树的相应方法实现即可。 + +Avl树的一种实现方式参考:https://github.com/emirpasic/gods/blob/master/trees/avltree/avltree.go + + +### 1.5.2 SB树 + + +对于平衡性而言,任何叔叔节点的子节点格式,不少于该节点的任何一个侄子节点子节点的个数 + + +``` +graph TD +c-->a +c-->e +a-->b +a-->d +e-->f +e-->g + +``` + +即a是一个叔叔节点,一定不少于f和g的子节点个数 + + +对于这种约束,也可保证任何节点的左右节点的个数不会有很大悬殊,即使高度不满足严格相减的绝对值小于2,也无伤大雅。整体仍然是O(logN) + +#### 1.5.2.1 SB树针对某个节点的平衡性处理 + +2007年,读高中的时候,承志峰研究出来的;常被用作比赛时,AVL树反而在ACM比赛中使用的相对少点 + + +也分为LL,LR,RR,RL四种类型。 + +当删除和插入某个节点,影响的节点的左孩子,不如其右孩子的左孩子节点个数多,RL形 + +当删除和插入某个节点,影响的节点的左孩子,不如其右孩子的右孩子节点个数多,RR形 + +当删除和插入某个节点,影响的节点的右孩子,不如其左孩子的左孩子节点个数多,LL形 + +当删除和插入某个节点,影响的节点的右孩子,不如其左孩子的右孩子节点个数多,LR形 + + +1、 对于LL形违规,对头结点x进行一次右旋,结束后,递归调用所以节点孩子个数发生变化的节点。右旋的时候,头结点x,和原x左孩子现在变成了头节点的b。这两个节点孩子个数发生了变化,要递归调用这两个节点。原因是原来不违规的节点,调整了位置后,pk的对象变了,要基于现在的结构再检查平衡性。这里虽然套了两个递归,整体仍然是O(1),证明略 + + +2、 RR形违规,和LL形违规类似处理; + + +**SB树平衡性相对于AVL树要模糊,所以平衡性调整比AVL的调整粒度要粗,也意味着SB树比AVL树速度要快,比赛常用。而且SB树可以设计成删除节点的时候,不进行平衡性调整,只有在添加节点的时候再进行平衡性调整,添加节点的时候有可能积压了很多的不平衡,但是我们有递归行为,仍然可以调整回平衡的状态;可能为棒状,有可能该次递归行为时间复杂度比较高,但是均摊下来仍然O(logN)水平;该结构比较重要** + + +**如果是违规的加点的左树高度超了,且左孩子的左右子节点个数相同,必须做LL形的调整,反之RR形同理** + + +- SB树的树结构版本 + +```Go +package main + +import "fmt" + +// SBTNode SBT树的节点类型 +type SBTNode struct { + // 该节点的Key + key string + // 该节点的V + value int + // 节点的左孩子 + l *SBTNode + // 节点的右孩子 + r *SBTNode + // 不同的key的数量 + size int +} + +type Comparator func(a, b interface{}) int + +// 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 +} + +type SBTree struct { + Root *SBTNode + Comparator Comparator +} + +// InitSBTTree 构建一个SBT树,返回头节点 +func InitSBTTree(key string, value int) *SBTree { + root := &SBTNode{ + key: key, + value: value, + size: 1, + } + + sbTree := &SBTree{ + Root: root, + Comparator: StringComparator, + } + return sbTree +} + +// 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 + } + + if cur.r != nil { + rSize = cur.r.size + } else { + rSize = 0 + } + + cur.size = lSize + rSize + 1 + return leftNode +} + +// 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 + } + + if cur.r != nil { + rSize = cur.r.size + } else { + rSize = 0 + } + + cur.size = lSize + rSize + 1 + return rightNode +} + +// maintain 对传入的cur节点,做平衡性调整。包含四种不平衡的调整策略:LL, LR, RL, RR +func (tree *SBTree) maintain(cur *SBTNode) *SBTNode { + if cur == nil { + return nil + } + + 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 +} + +// 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 +} + +// 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 +} + +// 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 +} + +// 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) + } +} + +// 在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 + } + + des.size = des.l.size + desRSize + 1 + // free cur memory + cur = des + } + } + return cur +} + +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) + } +} + +func (tree *SBTree) size() int { + if tree.Root == nil { + return 0 + } else { + return tree.Root.size + } +} + +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 +} + +// put 方法,有可能是新增有可能是覆盖更新 +func (tree *SBTree) put(key string, value int) error { + if len(key) == 0 { + return fmt.Errorf("invalid parameter") + } + + 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 +} + +func (tree *SBTree) remove(key string) error { + if len(key) == 0 { + return fmt.Errorf("invalid parameter") + } + + if b, e := tree.containsKey(key); e == nil && b { + tree.Root = tree.delete(tree.Root, key) + } + return nil +} + +func (tree *SBTree) getIndexKey(index int) (string, error) { + if index < 0 || index > tree.Root.size { + return "", fmt.Errorf("invalid parameter") + } + + return tree.getIndex(tree.Root, index + 1).key, nil +} + +func (tree *SBTree) getIndexValue(index int) (int, error) { + if index < 0 || index > tree.Root.size { + return -1, fmt.Errorf("invalid parameter") + } + + return tree.getIndex(tree.Root, index + 1).value, nil +} + +func (tree *SBTree) get(key string) (int, error) { + if len(key) == 0 { + return -1, fmt.Errorf("invalid parameter") + } + + lastNode := tree.findLastIndex(key) + if lastNode != nil && tree.Comparator(key, lastNode.key) == 0 { + return lastNode.value, nil + } else { + return -1, fmt.Errorf("not find") + } +} + +func (tree *SBTree) firstKey() (string, error) { + if tree.Root == nil { + return "", fmt.Errorf("not find because root is nil") + } + + cur := tree.Root + for cur.l != nil { + cur = cur.l + } + + return cur.key, nil +} + +func (tree *SBTree) lastKey() (string, error) { + if tree.Root == nil { + return "", fmt.Errorf("not find because root is nil") + } + + cur := tree.Root + for cur.r != nil { + cur = cur.r + } + return cur.key, nil +} + +func (tree *SBTree) floorKey(key string) (string, error) { + if len(key) == 0 { + return "", fmt.Errorf("invalid parameter") + } + + lastNoBigNode := tree.findLastNoBigIndex(key) + if lastNoBigNode == nil { + return "", fmt.Errorf("not find") + } else { + return lastNoBigNode.key, nil + } +} + +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 + } +} + +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)** + +跳表节点有多条往外指的指针,Node里面有一个List变量,类似于多叉树;跳表节点上也有可以比较的key,定义最小的key是null + +每个节点有多少个向下指针,随机指定,高层一定和所有节点最多的向下指针的条目数保持一致 + +跳表的最低层一定含有所有记录节点,概率上第二层有N/2个节点,概率上第三层会有N/4个节点... + +高层向底层寻找,实际上跳跃了很多的节点。这种机制跟输入的数据状况没关系,每一个节点随机层数,最后查找复杂度为O(logN) + + +```Go +package main + +import ( + "fmt" + "math" + "math/rand" + "time" +) + +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 + + sl.size = 0 + sl.levels = 1 + + return sl +} + +func (sl *skipList) Size() int { + return sl.size +} + +func (sl *skipList) Levels() int { + return sl.levels +} + +func (sl *skipList) Get(score int64) interface{} { + node := sl.findNode(score) + if node.score == score { + return node.val + } else { + return nil + } +} + +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 + + sl.insertAfter(f, curNode) + + rander := rand.New(rand.NewSource(time.Now().UnixNano())) + + curlevels := 1 + for rander.Intn(UP_LEVELS_TOTAL) < UP_LEVELS_ABILITY { + curlevels++ + if curlevels > sl.levels { + sl.newlevels() + } + + for f.up == nil { + f = f.pre + } + f = f.up + tmpNode := &skipListNode{score: score} + + curNode.up = tmpNode + tmpNode.down = curNode + sl.insertAfter(f, tmpNode) + + curNode = tmpNode + } + + sl.size++ +} + +func (sl *skipList) Remove(score int64) interface{} { + f := sl.findNode(score) + if f.score != score { + return nil + } + v := f.val + + for f != nil { + f.pre.next = f.next + f.next.pre = f.pre + f = f.up + } + return v +} + +func (sl *skipList) newlevels() { + nhead := &skipListNode{score: math.MinInt64} + ntail := &skipListNode{score: math.MaxInt64} + nhead.next = ntail + ntail.pre = nhead + + sl.head.up = nhead + nhead.down = sl.head + sl.tail.up = ntail + ntail.down = sl.tail + + sl.head = nhead + sl.tail = ntail + sl.levels++ +} + +func (sl *skipList) insertAfter(pNode *skipListNode, curNode *skipListNode) { + curNode.next = pNode.next + curNode.pre = pNode + pNode.next.pre = curNode + pNode.next = curNode +} + +func (sl *skipList) findNode(score int64) *skipListNode { + p := sl.head + + for p != nil { + if p.score == score { + if p.down == nil { + return p + } + 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 + } + } + } + return p +} + +func (sl *skipList) Print() { + + mapScore := make(map[int64]int) + + p := sl.head + for p.down != nil { + p = p.down + } + 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 + } + q = q.next + } + p = p.down + } +} + +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() +} +``` + + + +## 1.7 有序表例题实战 + +- 例题1 + +给定一些数组,长度不一。每个数组里面是有序的,可理解为二维数组,每一行有序,现在需要找一个a到b的区间,要求每一行都至少有一个数命中在该区间中。求满足这个这样条件的区间的最窄区间,如果存在多个最窄区间,返回区间位置起始最小的那个 + + + +> 解题流程:准备一个有序表,第一步,把每个数组中第0个树加入有序表,得到一个区间就是有序表中的最小值和最大值构成的区间,该区间已经可以包含每个数组中至少一个数在该区间内,但不一定是最小区间;第二步,找到有序表中最小的数在哪个数组中,弹出最小值,把该数组的下一个树加入有序表,看是否更新了最小区间,更小才更新,同样大不更新。重复。。。 + + +最后全局最小区间,就是我们要找的区间; + +> 解题思路:实质上解题流程,是在尝试每一个数字开头的情况下,哪个区间是最小的。以每一个数字去尝试,实质上是一种贪心思想,不去考虑数字不以数组中出现的区间,该区间一定不是最小的 + + +整个流程,只需要运用有序表的基本功能,原始的有序表已经能够满足需求,无需改写有序表,用系统实现的即可; + + +### 1.7.1 哪些情况下需要改写系统的有序表? + +- 例题-leetcode原题 + +给定一个数组arr,和两个整数a和b(a<=b) +求arr中有多少个子数组,累加和在[a,b]这个范围上 +返回达标的子数组数量 + + +例如a等于10,b等于30,在arr上,求0到i范围有多少子数组在10和30范围上,假设0带i和为100,反过来就是求0到i-1范围上有多少前缀和有多少落在70到90范围上; + + +所以,我们求0到p,p在0到i中间,前缀和在70到90上,我们可以得到p+1到i的累加和在10到30范围上。所以这题就是求0到p的前缀和有多少在我们根据a到b和0到i的累加和推出的新的范围上[sum-a, sum-b],就等于我们要求的个数 + + +我们把0到0的和,0到1的和,0到i的和。。。加入到我们的结构中去,求0到i结尾的子数组有多少个达标,就是求该结构上,有多少个前缀和落在了[sum-a, sum-b]的范围上;这个结构可以加入一个数字,且允许有重复值,给定一个范围[sum-a,sum-b]可以通过该结构返回加入的节点有多少个在这个范围上。例如加入到结构中的数字,有1,1,1,4,5,给定范围[1,5],返回6 + + +> 要实现这样的功能,系统实现的有序表,无法实现,一方面原始有序表无法加入重复数字,第二方面没有这样的方法返回个数。这样的方法,可以实现为,小于a的数有多少个,小于b的数有多少个,那么最终我们需要的个数就是a-b个 + +> 有序表结构本身比较重要,我们也经常使用系统实现的有序表,但是涉及到手动改有序表的实现,本身就已经比较难,而且面试出现的概率不是很高 \ No newline at end of file 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\345\216\237\347\220\206\345\217\212\346\211\251\345\261\225.md" "b/23-\343\200\212\350\277\233\351\230\266\343\200\213\346\234\211\345\272\217\350\241\250\345\216\237\347\220\206\345\217\212\346\211\251\345\261\225.md" deleted file mode 100644 index 4cf7bd3..0000000 --- "a/23-\343\200\212\350\277\233\351\230\266\343\200\213\346\234\211\345\272\217\350\241\250\345\216\237\347\220\206\345\217\212\346\211\251\345\261\225.md" +++ /dev/null @@ -1,2349 +0,0 @@ -[TOC] - -# 1 有序表原理及扩展 - -## 1.1 搜索二叉树 - -经典的搜索二叉树,是没有重复值的,任何节点为头的数,左孩子都比自己小,右孩子都比自己大 - - -允许重复值的改进的搜索二叉树,可以在每个节点上增加一个统计词频的数据项。表示出现了几次;但是不可相等的放到左右孩子上,搜索二叉树变平衡时,会影响后续的旋转 - - -1、搜索二叉树一定要说明以什么标准来排序 - -2、经典的搜索二叉树,树上没有重复的用来排序的key值 - -3、如果有重复节点的需求,可以在一个节点内部增加数据项 - - - -## 1.2 搜索二叉树的增删改查 - - -### 1.2.1 搜索二叉树的查找和添加 - -- 查找 - -搜索二叉树查询key(查询某个key存在还是不存在),当前节点比自己小,到右子树上去找,当前节点比自己大,到其左孩子上去找,越界,说明不存在 - - - -1、如果当前节点的value==key,返回true - -2、如果当前节点的valuekey,当前节点向右移动 - -4、如果当前节点变成null,返回false - - -- 添加 - -和查询过程一样,但当前节点滑到空的时候,就插入在这里 - - -- 删除 - -1、先找到key所在的节点 - -2、如果该节点没有左孩子、没有右孩子,直接删除即可(好理解) - -3、如果该节点有左孩子、没有右孩子,直接用左孩子顶替该节点(好理解) - -4、如果该节点没有左孩子、有右孩子,直接用右孩子顶替该节点(好理解) - -5、如果该节点有左孩子、有右孩子,用该节点后继节点顶替该节点(需要旋转调整,没法有左右孩子去替换,原因是左右孩子也有左右孩子) - -==一个节点的后继节点,就是该节点右孩子的最左的那个节点。== - -``` -graph TD -2-->1 -2-->5 -5-->3 -5-->10 -10-->8 -10-->13 -8-->6 -6-->7 -``` - -比如我要删除5节点,那么5节点的后继节点就是其右子树的最左的孩子,也就是6。把6替换掉5,6的右孩子给它父亲作为左孩子,得到 - -``` -graph TD -2-->1 -2-->6 -6-->3 -6-->10 -10-->8 -10-->13 -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; - } - - // 需要插入在该节点下面 - 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; - } - } - - Node newNode = createNode(element, insertParentNode, null, null); - if (insertParentNode.value > newNode.value) { - insertParentNode.left = newNode; - } else { - insertParentNode.right = newNode; - } - - 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; - } - } - - /** - * 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; - } - 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; - } - - /** - * @param element - * @return true if tree contains element. - */ - public boolean contains(int element) { - return search(element) != null; - } - - /** - * @return Minimum element in tree. - */ - public int getMinimum() { - return getMinimum(root).value; - } - - /** - * @return Maximum element in tree. - */ - public int getMaximum() { - return getMaximum(root).value; - } - - /** - * 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; - } - - /** - * @return Number of elements in the tree. - */ - public int getSize() { - return size; - } - - /** - * Tree traversal with printing element values. In order method. - */ - public void printTreeInOrder() { - printTreeInOrder(root); - } - - /** - * Tree traversal with printing element values. Pre order method. - */ - public void printTreePreOrder() { - printTreePreOrder(root); - } - - /** - * Tree traversal with printing element values. Post order method. - */ - public void printTreePostOrder() { - printTreePostOrder(root); - } - - /*-------------------PRIVATE HELPER METHODS-------------------*/ - - private void printTreeInOrder(Node entry) { - if (entry != null) { - printTreeInOrder(entry.left); - if (entry.value != null) { - System.out.println(entry.value); - } - printTreeInOrder(entry.right); - } - } - - private void printTreePreOrder(Node entry) { - if (entry != null) { - if (entry.value != null) { - System.out.println(entry.value); - } - printTreeInOrder(entry.left); - printTreeInOrder(entry.right); - } - } - - private void printTreePostOrder(Node entry) { - if (entry != null) { - printTreeInOrder(entry.left); - printTreeInOrder(entry.right); - if (entry.value != null) { - System.out.println(entry.value); - } - } - } - - protected Node getMinimum(Node node) { - while (node.left != null) { - node = node.left; - } - return node; - } - - 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; - } - } - - // -------------------------------- TREE PRINTING - // ------------------------------------ - - public void printTree() { - printSubtree(root); - } - - public void printSubtree(Node node) { - if (node.right != null) { - printTree(node.right, true, ""); - } - printNodeValue(node); - if (node.left != null) { - printTree(node.left, false, ""); - } - } - - private void printNodeValue(Node node) { - if (node.value == null) { - System.out.print(""); - } else { - System.out.print(node.value.toString()); - } - System.out.println(); - } - - 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 ? " | " : " ")); - } - } - - 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; - } - - } -} - -``` - - -## 1.3 传统搜索二叉树存在的问题 - - -1)基础的搜索二叉树,添加、删除时候不照顾平衡性 - -2)数据状况很差时,性能就很差 - - -==给搜索二叉树引入两个动作:左旋、右旋== - - -输入状况,决定性能。比如输入状况构建出来的树,严重不平衡。极端情况是只有一条通往底部的路径,高度为n; - -平衡二叉树的定义,任何子树,左子树的高度和右子树的高度差,不大于1。所以对于n个节点,平衡二叉树的高度,就是logN - - -### 1.3.1 平衡搜索二叉树 - - -平衡搜索二叉树,就是在插入和删除的过程中,动态的保持平衡性。保持平衡的代价保持在logN。平衡搜索二叉树的实现由很多,红黑树只是其中一种 - - -### 1.3.2 左旋和右旋 - -左旋和右旋针对头结点而言的,即对哪个头结点进行左旋还是右旋;顾名思义如果对头结点为a的子树右旋,那么a倒到右边,a的左孩子顶上来,到a原来的位置上去。a原来左孩子的右孩子,现在当做a的左孩子,如下图 - - -``` -graph TD -a-->b -a-->c -c-->d -c-->e -b-->f -b-->g -``` - -a右旋,得到: - - -``` -graph TD -b-->f -b-->a -a-->g -a-->c -c-->d -c-->e -``` - -同理,a左旋,得到: - -``` -graph TD -c-->a -c-->e -a-->b -a-->d -b-->f -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; - } - -} - -``` - - - -## 1.4 有序表 - - -在Java中,就是TreeMap,有序表和Hash表(HashMap)的操作类似,但是有序表中自动把我们的key排好了顺序,我们也可以传入比较器,自定义对比和排序的规则;我们可以通过TreeMap直接得到最大节点和最小节点,也可以获取大于某个值的第一个Key是什么等等 - - -为什么TreeMap可以做到有序,原因是TreeMap的底层是一个平衡搜索二叉树 - - -- Hash表和有序表对比 - -1、HashMap的所有操作是O(1)的,TreeMap的所有操作是O(logN) - -2、使用有序表的Key必须是可以比较的,没法自然比较的需要传入自定义比较器 - - -## 1.5 有序表的实现(AVL树,SB树,红黑树) - -在有序表中,有序表是一种规范,类似于接口名。规范为key要按序组织,所有操作要是O(logN)等。各种树结构可以实现有序表的功能。其中红黑树只是有序表的一个实现 - - -==AVL树,SB树,红黑树,都是有序表的一种实现。都是平衡搜索二叉树,这三种树在功能和时间复杂度上几乎无差别,在实现细节上也就是在常数时间上,会有差别。三种树调整检查哪些节点的平衡性相同,下文进行说明。每种树针对每个节点的平衡性调整不同,但是都是使用左旋和右旋两个动作== - -- AVL树,SB树,红黑树的共性 - -1)都是搜索二叉树 - -2)插入、删除、查询(一切查询)搜索二叉树怎么做,这些结构都这么做 - -3)使用调整的基本动作都只有左旋、右旋 - -4)插入、删除时,从最底层被影响到的节点开始,对往上路径的节点做平衡性检查 - -5)因为只对一条向上路径的每个节点做O(1)的检查和调整,所以可以做到O(logN) - - -- AVL树,SB树,红黑树不同之处 - -1)平衡性的约束不同 - -AVL树最严格、SB树稍宽松、红黑树最宽松 - -2)插入、删除和搜索二叉树一样,但是额外,做各自的平衡性调整。各自的平衡性调整所使用的动作都是左旋或者右旋 - - - - -### 1.5.1 AVL树 - -是这三个平衡搜索二叉树中,平衡性最严格的,左树高度和右树高度的绝对值,严格小于2 - -AVL树在插入节点的时候,只会向上检查节点的平衡性有没有被破坏。删除节点也一样,只会检查删除的那个节点向上的那条链上的节点有没被破坏。删除的时候,如果被删除的节点没有左右孩子那么直接检查,如果有右孩子,是去检查后继节点原来所在的位置的向上节点的平衡性 - -==实质上三种树删除和插入节点,检查哪些节点需要调整平衡都是这样的查找规则,对于删除来说,只有左树和只有右树没影响,如果左右树都存在,是去检查后继节点原来所在的位置向上的平衡性。只是具体到某个节点平衡性的处理上,三种树不一样== - - -#### 1.5.1.1 AVL树针对某个节点的平衡性处理 - -1、该节点左树高度和右树高度差的绝对值|L-R|,小于2。不违规,无须调整 - -2、|L-R|大于1,说明要不然左树高度大了,要不然右树高度大了。而且之前的每一步都进行了调整,所以高度差不会超过2。高度不平衡对应四种情况,被称为RR形违规,RL形违规,LL形违规,LR形违规。首字母表示左树变高了还是右树变高了,比如RR和RL都表示经过插入或者删除,右树的高度变大了 - - -RR表示,右子节点的右树过长,RL表示右子节点的左树过长。同理LL表示左子节点的左子树高度过长导致,LR表示左子节点的右树高度过长导致的不平衡 - -- LL形违规,对该节点进行一次右旋即可 - -``` -graph TD -x-->y -x-->p -y-->z -y-->t -z-->k -``` - -右旋后得到: - -``` -graph TD -y-->z -y-->x -z-->k -x-->t -x-->p -``` - - -- 同理RR形违规,对该节点进行一次左旋即可 - - -- LR形,让底部的孙子节点,来到顶部来。 - -下面例子,想办法让x的孙子节点t节点来到顶部,先对y节点进行一次左旋,再对x节点做一次右旋 - -``` -graph TD -x-->y -x-->p -y-->z -y-->t -t-->k -``` - -先对y节点进行一次左旋 - -``` -graph TD -x-->t -x-->p -y-->z -t-->y -t-->k -``` - -再对x节点做一次右旋 - -``` -graph TD -t-->y -t-->x -x-->k -x-->p -y-->z -``` - -原x的孙子节点t此时,调整到的顶部 - -- 同理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); - } - - // 把当前node向上变为父节点,向上窜,继续调整平衡 - node = (AVLNode) parent; - } - } - - /** - * Rotates to left side. - */ - private Node avlRotateLeft(Node node) { - Node temp = super.rotateLeft(node); - - 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); - } - } - -} - -``` - - -### 1.5.2 SB树 - - -对于平衡性而言,任何叔叔节点的子节点格式,不少于该节点的任何一个侄子节点子节点的个数 - - -``` -graph TD -c-->a -c-->e -a-->b -a-->d -e-->f -e-->g - -``` - -即a是一个叔叔节点,一定不少于f和g的子节点个数 - - -对于这种约束,也可保证任何节点的左右节点的个数不会有很大悬殊,即使高度不满足严格相减的绝对值小于2,也无伤大雅。整体仍然是O(logN) - -#### 1.5.2.1 SB树针对某个节点的平衡性处理 - -2007年,读高中的时候,承志峰研究出来的;常被用作比赛时,AVL树反而在ACM比赛中使用的相对少点 - - -也分为LL,LR,RR,RL四种类型。 - -当删除和插入某个节点,影响的节点的左孩子,不如其右孩子的左孩子节点个数多,RL形 - -当删除和插入某个节点,影响的节点的左孩子,不如其右孩子的右孩子节点个数多,RR形 - -当删除和插入某个节点,影响的节点的右孩子,不如其左孩子的左孩子节点个数多,LL形 - -当删除和插入某个节点,影响的节点的右孩子,不如其左孩子的右孩子节点个数多,LR形 - - -1、 对于LL形违规,对头结点x进行一次右旋,结束后,递归调用所以节点孩子个数发生变化的节点。右旋的时候,头结点x,和原x左孩子现在变成了头节点的b。这两个节点孩子个数发生了变化,要递归调用这两个节点。原因是原来不违规的节点,调整了位置后,pk的对象变了,要基于现在的结构再检查平衡性。这里虽然套了两个递归,整体仍然是O(1),证明略 - - -2、 RR形违规,和LL形违规类似处理; - - -==SB树平衡性相对于AVL树要模糊,所以平衡性调整比AVL的调整粒度要粗,也意味着SB树比AVL树速度要快,比赛常用。而且SB树可以设计成删除节点的时候,不进行平衡性调整,只有在添加节点的时候再进行平衡性调整,添加节点的时候有可能积压了很多的不平衡,但是我们有递归行为,仍然可以调整回平衡的状态;可能为棒状,有可能该次递归行为时间复杂度比较高,但是均摊下来仍然O(logN)水平;该结构比较重要== - - -==如果是违规的加点的左树高度超了,且左孩子的左右子节点个数相同,必须做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; - } - } - - 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; - } - - // 左旋操作和右旋操作同理 - 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; - } - - // 调整 - 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; - } - - 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; - } - - 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; - } - - 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; - } - - // 现在,以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); - } - } - - // 在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; - } - - 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); - } - } - - public int size() { - return root == null ? 0 : root.size; - } - - 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; - } - - // 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); - } - } - - public void remove(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - if (containsKey(key)) { - root = delete(root, key); - } - } - - public K getIndexKey(int index) { - if (index < 0 || index >= this.size()) { - throw new RuntimeException("invalid parameter."); - } - return getIndex(root, index + 1).key; - } - - public V getIndexValue(int index) { - if (index < 0 || index >= this.size()) { - throw new RuntimeException("invalid parameter."); - } - return getIndex(root, index + 1).value; - } - - 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; - } - } - - public K firstKey() { - if (root == null) { - return null; - } - SBTNode cur = root; - while (cur.l != null) { - cur = cur.l; - } - return cur.key; - } - - public K lastKey() { - if (root == null) { - return null; - } - SBTNode cur = root; - while (cur.r != null) { - cur = cur.r; - } - 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; - } - - public K ceilingKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - SBTNode lastNoSmallNode = findLastNoSmallIndex(key); - return lastNoSmallNode == null ? null : lastNoSmallNode.key; - } - - } - - // for test - public static void printAll(SBTNode head) { - System.out.println("Binary Tree:"); - printInOrder(head, 0, "H", 17); - System.out.println(); - } - - // 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); - - } - -} -``` - -- 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; - } - - 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; - } - - 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; - } - - 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; - } - - 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; - } - - 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; - } - - 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); - } - } - - 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); - } - } - - public int size() { - return len; - } - - 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; - } - - 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); - } - } - - public K getIndexKey(int index) { - if (index < 0 || index >= len) { - throw new RuntimeException("invalid parameter."); - } - return keys.get(getIndex(root, index + 1)); - } - - public V getIndexValue(int index) { - if (index < 0 || index >= len) { - throw new RuntimeException("invalid parameter."); - } - return values.get(getIndex(root, index + 1)); - } - - 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; - } - } - - public K firstKey() { - int cur = root; - while (left[cur] != 0) { - cur = left[cur]; - } - return cur == 0 ? null : keys.get(cur); - } - - public K lastKey() { - int cur = root; - while (right[cur] != 0) { - cur = right[cur]; - } - return cur == 0 ? null : keys.get(cur); - } - - public K floorKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - int lastNoBigIndex = findLastNoBigIndex(key); - return lastNoBigIndex == 0 ? null : keys.get(lastNoBigIndex); - } - - public K ceilingKey(K key) { - if (key == null) { - throw new RuntimeException("invalid parameter."); - } - int lastNoSmallIndex = findLastNoSmallIndex(key); - return lastNoSmallIndex == 0 ? null : keys.get(lastNoSmallIndex); - } - } - - public static void main(String[] args) { - SizeBalancedTreeMap sbt = new SizeBalancedTreeMap(10000); - - sbt.put("pos", 512); - sbt.put("zyp", 7123); - sbt.put("unz", 542); - sbt.put("abc", 5113); - sbt.put("yzk", 665); - sbt.put("fgi", 38776); - sbt.put("bke", 2500540); - sbt.put("lmn", 44334); - sbt.put("abc", 11); - sbt.put("abc", 111); - - for (int i = 0; i < sbt.size(); i++) { - System.out.println(sbt.getIndexKey(i) + " , " + sbt.getIndexValue(i)); - } - System.out.println(sbt.get("abc")); - System.out.println(sbt.firstKey()); - System.out.println(sbt.lastKey()); - System.out.println(sbt.floorKey("bke")); - System.out.println(sbt.ceilingKey("bke")); - System.out.println(sbt.floorKey("ooo")); - System.out.println(sbt.ceilingKey("ooo")); - System.out.println(sbt.floorKey("aaa")); - System.out.println(sbt.ceilingKey("aaa")); - System.out.println(sbt.floorKey("zzz")); - System.out.println(sbt.ceilingKey("zzz")); - - } - -} - -``` - - -### 1.5.3 红黑树 - -1、 节点有红有黑 - -2、头节点和叶子节点是黑 - -3、红节点的子,一定要是黑节点 - -4、从任何节点到他的子节点,所有的路径中黑节点都是一样多的 - - -从第三点和第四点可以推出,任何长链(黑红交替)和任何段链(都为黑)保证黑一样多,那么长链的长度一定不会比短链长度的二倍还要大。实质上上面条件的目的也是保证最长的链不要比最短的链2倍还多,一定程度上保证了平衡性 - - - -#### 1.5.3.1 红黑树针对某个节点的平衡性处理 - -红黑树本质上也是搜索二叉树,搜索二叉树怎么增加和删除节点,红黑树相同;同样的加完节点,删除完节点,之后从受影响的节点往上都进行平衡性检查,几种有序表的实现只有检查平衡性的评估指标不同 - - -红黑树平衡性检查,插入的情况下,违规的情况有5种,删除的情况下违规的情况有8种。红黑树自身的这些规定,会引出来13种违规情况下的节点调整。发明红黑树的人把13种情况都研究明白了,现在让我们学,哈哈 - - -==面试场上看到问红黑树相关的内容,可以回答本质上也是搜索二叉树,不平衡性较多有13种,AVL和SB树都只有四种LL,LR,RR,RL。比红黑树简单。如果面试官一定要问哪5种,哪8种,那么这个面试官有问题,这种不平衡性可以查文档来获取。这种就属于面试官就是不想让你过,纠结哪8种,哪5种,纯粹是磨工夫== - - -红黑树这么麻烦,好处在哪,为什么这么著名,原因在于红黑树的扰动小。AVL由于平衡性特别清晰,增加和删除节点特别灵敏,扰动大。SB和红黑树的平衡性相对模糊,而且SB在删除的节点的时候,可以不进行平衡性调整,扰动小 - - - -有些场景,就是需要扰动小的调整,比如硬盘io的时候,每个树的节点,就是一块硬盘区域。硬盘删除,更新,插入等,如果时间复杂度评估指标相等,还是要选择扰动小的结构。内存中无所谓,扰动大小都可 - - - -如果每次插入和删除,行为代价都非常的高,红黑树都不考虑用,而是选择用底层硬盘结构的树,不如B树,和B+树,234树。这些树是多叉树。B树和B+树牺牲了一定的查询效率,虽然也是O(logN),常数项很大,但是没有AVL,SB,和红黑树的效率高。比如数据库组织的一些树,就是考虑到少读写硬盘,就是用的B+树 - - -红黑树,在(AVL,SB) 和 硬盘的树(B,B+)树之间达到平衡。各有取舍 - - -##### Redis为什么选择跳表的结构? - -Redis为什么选择跳表的结构,而不是AVL和SB树呢,实质上可以选择,但是考虑到redis可能需要对有序表进行序列化的要求,SkipList就是多层的线性结构,比较好序列化。AVL和SB是个结构化的东西,不好序列化;一种技术的选型,需要根据自己的生存状态去选择的 - - - -==三种树的平衡性保证策略不同,各自实现各自的平衡性,但是三个树都只有左旋和右旋两种调整策略== - - - -## 1.6 跳表SkipList(也可实现有序表功能) - - -==最烧脑的结构== - -跳表也可实现有序表的功能,但是跳表不是搜索二叉树,实现机制跟二叉树也没关系 - - -==跳表实现有序表,比较好实现,思想也相对更先进O(logN)== - - -跳表节点有多条往外指的指针,Node里面有一个List变量,类似于多叉树;跳表节点上也有可以比较的key,定义最小的key是null - - - -每个节点有多少个向下指针,随机指定,高层一定和所有节点最多的向下指针的条目数保持一致 - - -跳表的最低层一定含有所有记录节点,概率上第二层有N/2个节点,概率上第三层会有N/4个节点... - -高层向底层寻找,实际上跳跃了很多的节点。这种机制跟输入的数据状况没关系,每一个节点随机层数,最后查找O(logN) - - - -```Java -package class06; - -import java.util.ArrayList; - -public class Code03_SkipListMap { - - // 跳表的节点定义,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; - - 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>(); - } - - // 遍历的时候,如果是往右遍历到的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); - } - - // 判断当前node的key可传入的key是否相等 - public boolean isKeyEqual(K otherKey) { - return (key == null && otherKey == null) - || (key != null && otherKey != null && key.compareTo(otherKey) == 0); - } - - } - - 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; - } - - // 从最高层开始,一路找下去, - // 最终,找到第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; - } - - // 在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; - } - - 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); - } - - // 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--; - } - } - } - - 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; - } - - 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--; - } - } - } - - public K firstKey() { - return head.nextNodes.get(0) != null ? head.nextNodes.get(0).key : null; - } - - 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--; - } - return cur.key; - } - - public K ceillingKey(K key) { - if (key == null) { - return null; - } - SkipListNode less = mostRightLessNodeInTree(key); - SkipListNode next = less.nextNodes.get(0); - return next != null ? next.key : null; - } - - 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; - } - - public int size() { - return size; - } - - } - - // 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; - } - System.out.println(); - } - } - - 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")); - - } - -} - -``` - - - -## 1.7 有序表例题实战 - -- 例题1 - -给定一些数组,长度不一。每个数组里面是有序的,可理解为二维数组,每一行有序,现在需要找一个a到b的区间,要求每一行都至少有一个数命中在该区间中。求满足这个这样条件的区间的最窄区间,如果存在多个最窄区间,返回区间位置起始最小的那个 - - - -> 解题流程:准备一个有序表,第一步,把每个数组中第0个树加入有序表,得到一个区间就是有序表中的最小值和最大值构成的区间,该区间已经可以包含每个数组中至少一个数在该区间内,但不一定是最小区间;第二步,找到有序表中最小的数在哪个数组中,弹出最小值,把该数组的下一个树加入有序表,看是否更新了最小区间,更小才更新,同样大不更新。重复。。。 - - -最后全局最小区间,就是我们要找的区间; - - - -> 解题思路:实质上解题流程,是在尝试每一个数字开头的情况下,哪个区间是最小的。以每一个数字去尝试,实质上是一种贪心思想,不去考虑数字不以数组中出现的区间,该区间一定不是最小的 - - -整个流程,只需要运用有序表的基本功能,原始的有序表已经能够满足需求,无需改写有序表,用系统实现的即可; - - -### 1.7.1 哪些情况下需要改写系统的有序表? - -- 例题-leetcode原题 - -给定一个数组arr,和两个整数a和b(a<=b) -求arr中有多少个子数组,累加和在[a,b]这个范围上 -返回达标的子数组数量 - - -例如a等于10,b等于30,在arr上,求0到i范围有多少子数组在10和30范围上,假设0带i和为100,反过来就是求0到i-1范围上有多少前缀和有多少落在70到90范围上; - - -所以,我们求0到p,p在0到i中间,前缀和在70到90上,我们可以得到p+1到i的累加和在10到30范围上。所以这题就是求0到p的前缀和有多少在我们根据a到b和0到i的累加和推出的新的范围上[sum-a, sum-b],就等于我们要求的个数 - - -我们把0到0的和,0到1的和,0到i的和。。。加入到我们的结构中去,求0到i结尾的子数组有多少个达标,就是求该结构上,有多少个前缀和落在了[sum-a, sum-b]的范围上;这个结构可以加入一个数字,且允许有重复值,给定一个范围[sum-a,sum-b]可以通过该结构返回加入的节点有多少个在这个范围上。例如加入到结构中的数字,有1,1,1,4,5,给定范围[1,5],返回6 - - -> 要实现这样的功能,系统实现的有序表,无法实现,一方面原始有序表无法加入重复数字,第二方面没有这样的方法返回个数。这样的方法,可以实现为,小于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树完全可以替换,没任何差别 - - - - - - - - - - - - - - - - - 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) + } +} ``` 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" deleted file mode 100644 index 6786fdb..0000000 --- "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" +++ /dev/null @@ -1,358 +0,0 @@ -```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; - } -} -``` \ No newline at end of file diff --git "a/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" "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" new file mode 100644 index 0000000..88070c6 --- /dev/null +++ "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" @@ -0,0 +1,325 @@ +```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 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" deleted file mode 100644 index ba96c1b..0000000 --- "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" +++ /dev/null @@ -1,676 +0,0 @@ -- 基础 - -```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; - } - } - - - /** - * 递归先序遍历 - * - * @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; - } -} -``` - -- 二叉树相关练习 - -```java -package com.xiaodai.algorithm; - -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.Queue; - -/** - * Author :dai - * Date :2020/12/30 2:56 下午 - * Description:解决二叉树的具体问题,递归思维的建立 - */ -public class TreeSolvingUtil { - - /** - * 节点信息 - */ - public static class Node { - public int value; - public Node left; - public Node right; - - public Node(int data) { - this.value = data; - } - } - - - /** - * 1、判断二叉树是否是平衡的 - * - * @param head - * @return - */ - public static boolean isBalanced(Node head) { - - return isBalancedProcess(head).isBalanced; - - } - - /** - * 1. 递归调用,head传入整体需要返回一个信息 - * 2. 解决当前节点的Info信息怎么得来 - * - * @param head - * @return - */ - private static IsBalancedInfo isBalancedProcess(Node head) { - - if (head == null) { - return new IsBalancedInfo(true, 0); - } - - IsBalancedInfo leftInfo = isBalancedProcess(head.left); - - IsBalancedInfo rightInfo = isBalancedProcess(head.right); - - // 当前节点的高度,等于左右树最大的高度,加上当前节点高度1 - int currentHeight = Math.max(leftInfo.height, rightInfo.height) + 1; - - boolean isBalanced = true; - - // 左树不平衡,或者右树不平衡,或者左右树高度差大于1 - if (!leftInfo.isBalanced || !rightInfo.isBalanced || Math.abs(leftInfo.height - rightInfo.height) > 1) { - isBalanced = false; - } - - return new IsBalancedInfo(isBalanced, currentHeight); - } - - /** - * 递归过程中需要整合的信息体 - */ - public static class IsBalancedInfo { - - // 是否平衡 - boolean isBalanced; - - // 高度多少 - int height; - - IsBalancedInfo(boolean b, int height) { - this.isBalanced = b; - this.height = height; - } - } - - - /** - * 2、二叉树中,获取任意两个节点的最大距离 - * - * @param head - * @return - */ - public static int maxDistance(Node head) { - - return maxDistanceProcess(head).maxDistance; - - } - - /** - * 任意节点需要返回的信息体:以该节点为头的高度,整棵树的最大距离 - */ - public static class MaxDistanceInfo { - public int maxDistance; - public int height; - - public MaxDistanceInfo(int dis, int h) { - this.maxDistance = dis; - this.height = h; - } - } - - private static MaxDistanceInfo maxDistanceProcess(Node head) { - - if (head == null) { - return new MaxDistanceInfo(0, 0); - } - - MaxDistanceInfo leftInfo = maxDistanceProcess(head.left); - - MaxDistanceInfo rightInfo = maxDistanceProcess(head.right); - - // 当前节点为头的情况下,高度等于左右树较大的高度,加上1 - int height = Math.max(leftInfo.height, rightInfo.height) + 1; - - // 当前节点为头的情况下,最大距离等于,左右树距离较大的那个距离(与当前节点无关的情况) - // 和左右树高度相加再加上当前节点距离1的距离(与当前节点有关的情况)取这两种情况较大的那个 - int maxDistance = Math.max(Math.max(leftInfo.maxDistance, rightInfo.maxDistance) - , (leftInfo.height + rightInfo.height + 1)); - - return new MaxDistanceInfo(maxDistance, height); - } - - - /** - * 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; - } - - -} -``` \ No newline at end of file diff --git "a/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" "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" new file mode 100644 index 0000000..d367b0c --- /dev/null +++ "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" @@ -0,0 +1,451 @@ +- 二叉树基础 + +```Go +package main + +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) +} + +// 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) + } + } + } +} + +// 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 + } + } + } +} + +// 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() +} + +// 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() +} + +// 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) + } +} + +// 10、根据先序序列化的结果,反序列化成一颗树 +func buildByPreQueue(prelist []string) *Node { + if len(prelist) == 0 { + return nil + } + + 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 +} +``` + +- 二叉树应用 + +```Go +package main + +import "math" + +// Node 二叉树的节点定义 +type Node struct { + Value int + Left *Node + Right *Node +} + + +// IsBalanced 1、判断二叉树是否是平衡的 +func IsBalanced(head *Node) bool { + return isBalancedProcess(head).isBalanced +} + +// 递归过程信息 +type isBalancedInfo struct { + isBalanced bool + height int +} + +// 递归调用,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, + } +} + +// MaxDistance 2、二叉树中,获取任意两个节点的最大距离 +func MaxDistance(head *Node) int { + return maxDistanceProcess(head).maxDistance +} + +type maxDistanceInfo struct { + maxDistance int + height int +} + +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, + } +} + +// IsFull 3、判断一颗树是否是满二叉树 +func IsFull(head *Node) bool { + if head == nil { + return true + } + + all := isFullProcess(head) + + return (1 << all.height) - 1 == all.nodes +} + +// 判断一棵树是否是满二叉树,每个节点需要返回的信息 +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, + } +} + +// GetMaxLength 4、找到二叉树中节点和等于sum的最长路径 +func GetMaxLength(head *Node, sum int) int { + sumMap := make(map[int]int, 0) + sumMap[0] = 0 + + return preOrder(head, sum, 0, 1, 0, sumMap) +} + +func preOrder(head *Node, sum int, preSum int, level int, maxLen int, sumMap map[int]int) int { + if head == nil { + return maxLen + } + + curSum := preSum + head.Value + if _, ok := sumMap[curSum]; !ok { + sumMap[curSum] = level + } + + if v, ok := sumMap[curSum - sum]; ok { + maxLen = int(math.Max(float64(level - v), float64(maxLen))) + } + + maxLen = preOrder(head.Left, sum, curSum, level + 1, maxLen, sumMap) + maxLen = preOrder(head.Right, sum, curSum, level + 1, maxLen, sumMap) + + if level == sumMap[curSum] { + delete(sumMap, curSum) + } + + return maxLen +} + +// 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) +} + +// 一棵树是原始树 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 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" deleted file mode 100644 index 6c4770b..0000000 --- "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" +++ /dev/null @@ -1,318 +0,0 @@ -```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); - } - - -} -``` \ No newline at end of file diff --git "a/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" "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" new file mode 100644 index 0000000..4520143 --- /dev/null +++ "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" @@ -0,0 +1,260 @@ +```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 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" deleted file mode 100644 index ecc3c2b..0000000 --- "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" +++ /dev/null @@ -1,293 +0,0 @@ -```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]; - } - - -} -``` - diff --git "a/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" "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" new file mode 100644 index 0000000..da8bf17 --- /dev/null +++ "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" @@ -0,0 +1,278 @@ +```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] +} +``` + diff --git a/README.md b/README.md index 0c301d1..ff80dde 100644 --- a/README.md +++ b/README.md @@ -2,39 +2,39 @@ ## 笔记简介 -* 左神算法课笔记记录,个人增加了总结性的内容。算法的提高还需结合大量的刷题 -* 出发点是,算法学习内容杂且难,整理一份尽可能还原网课的笔记,当成工具书查阅帮助复习 +* 对于常用数据结构及算法的系统性总结,java版本参考algorithm-note-java分支,master分支基于Golang实现。 +* 出发点是,算法内容杂且难,系统性整理当成工具书查阅,可以有效帮助复习 * 如果本系列对您有用,求个star~ ## 笔记阅读传送门 -- 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)