-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.json
1 lines (1 loc) · 498 KB
/
search.json
1
[{"title":"LeetCode Hot100","url":"/2023/12/01/LeetCode/LeetCodeHot100/","content":"\n盛最多水的容器\nhttps://leetcode.cn/problems/container-with-most-water/?envType=study-plan-v2&envId=top-100-liked\n如果使用两层for循环会导致超时。\n使用双指针从左右边界向内收缩,依次缩减范围,直到两个指针相遇,具体收缩步骤:\n\n\n计算体积,比较大小\n比较左右指针所在位置的高度\n高度低的向内收缩\n\n🍰 证明:\n假设左右指针分别为i, j所在位置的高度为x, y,用S(i, j)表示指针位置形成的体积大小。这里假设x < y。如果按照上述收缩思路的话,应该是i向右移,形成的体积大小是S(i + 1, j)。这里会有疑问的是丢失了S(i, j - 1), S(i, j - 2), ..., S(i, i + 1)的状态。下面证明这些状态都小于S(i, j)。\n首先设j - i的长度为w,然后丢失状态的所有容器的长度(j - t) - i设为w'都一定小于w。\n丢失状态中的i指针不动,高度为x,右指针向内收缩,高度设为y',可以知道y'和y的大小关系不确定。分为两种情况:\n\ny' > y,那么S(i, j - t)的体积为w' * x,一定小于S(i, j) = w * x。\ny' <= y,那么S(i, j - t)的体积为min(x, y') * w'\n\nx > y',S(i, j - t) = y' * w'一定小于w * x\nx <= y',S(i, j - t) = x * w'一定小于w * x\n\n\n所以如果移动高位置的指针,一定会导致容器体积减小,移动低位置的可能会使得容器体积增大。\nclass Solution { public int maxArea(int[] height) { int result = 0; for (int i = 0, j = height.length - 1; i < j;) { result = Math.max(result, Math.min(height[i], height[j]) * (j - i)); if (height[i] < height[j]) { i++; } else { j--; } } return result; }}\n三数之和\nhttps://leetcode.cn/problems/3sum/description/?envType=study-plan-v2&envId=top-100-liked\n如果使用三层for循环会超时。\n这里使用双指针,首先对数组进行排序,然后固定指针k,取另外两个指针i j分别在k + 1, nums.length - 1的位置,然后判断:\n\nnums[k] + nums[i] + nums[j] > 0则整体偏大,需要减小,可以让j--\nnums[k] + nums[i] + nums[j] < 0则整体偏小,需要增大,可以让i++\n否则满足条件,加入结果列表\n\n这里需要注意要排除重复的三元组,因此在移动指针时需要判断后续判断的值是否和当前值冲突\nclass Solution { public List<List<Integer>> threeSum(int[] nums) { Arrays.sort(nums); List<List<Integer>> resultList = new ArrayList<>(); for (int k = 0; k < nums.length; k++) { // 排除重复元素,如果k和k-1位置的数一样,那么后续计算的结果一定一样,因此需要排除 if (k > 0 && nums[k] == nums[k - 1]) { continue; } // 如果nums[k] > 0,那么nums[i]和nums[j]一定 > 0,结果一定 > 0 if (nums[k] > 0) { return resultList; } for (int i = k + 1, j = nums.length - 1; i < j;) { int temp = nums[i] + nums[j] + nums[k]; if (temp > 0) { // 跳过重复数值,只取重复的最后一个 while (i < j && nums[j] == nums[--j]); } else if (temp < 0) { while (i < j && nums[i] == nums[++i]); } else { resultList.add(Arrays.asList(nums[k], nums[i], nums[j])); while (i < j && nums[i] == nums[++i]); while (i < j && nums[j] == nums[--j]); } } } return resultList; }}\n最长连续序列\nhttps://leetcode.cn/problems/longest-consecutive-sequence/description/?envType=study-plan-v2&envId=top-100-liked\n本题要求时间复杂度为O(n),如果直接排序的话复杂度达到O(nlogn)。\n要找出连续序列的长度,可以换个思路,如果找到一个数nums[i],然后将这个数一直+1,加完后的数在数组里还能找到那就说明连续序列的长度可以增加\n1。因此满足这个条件的话需要使用到哈希,将所有的数保存在一个集合中,方便查询。\n这里的一个优化是如果当前数减 1\n之后还能在数组中找到,那么可以直接跳过,因为减 1\n之后的数在计算序列长度时已经包含了当前数,从当前数开始计算的长度一定不如减\n1 之后的数计算的序列长度长。\nclass Solution { public int longestConsecutive(int[] nums) { Set<Integer> numSet = new HashSet<>(); for (int num : nums) { numSet.add(num); } int result = 0; for (int num : nums) { // 如果包含减1之后的数,那么直接跳过,因为之前已经计算过 if (numSet.contains(num - 1)) { continue; } int tempResult = 1; int temp = num; // 如果加1后还在数组里能找到,序列长度加1 while (numSet.contains(++temp)) { tempResult++; } // 保存最大的序列长度 result = Math.max(result, tempResult); } return result; }}\n接雨水\nhttps://leetcode.cn/problems/trapping-rain-water/description/?envType=study-plan-v2&envId=top-100-liked\n根据题意,最两侧的柱子不能接到雨水,只能作为墙壁。考虑按列求,单独考虑每个柱子可以接到的雨水。\n当前柱子可以接到的雨水由当前柱子左侧和右侧的最高的柱子包裹起来,可以保证接到雨水,而接到的雨水的多少则是右两则最高的柱子中的矮的决定(木桶效应)。如果矮的柱子比当前柱子高,那么接到的雨水就是高度的差值,否则接不到雨水。\n那么现在的问题是怎么快速求出当前柱子左侧最高的高度和当前柱子右侧最高的高度。\n\n动态规划\n\n使用一个数组记录当前位置左侧(不包括自身)最高的高度是多少,即maxLeft[i] = max(maxLeft[i - 1], height[i - 1]),右侧同理\n这会使用到O(n)的空间复杂度\nclass Solution { public int trap(int[] height) { int[] maxLeft = new int[height.length]; int[] maxRight = new int[height.length]; for (int i = 1; i < height.length; i++) { maxLeft[i] = Math.max(maxLeft[i - 1], height[i - 1]); } for (int i = height.length - 2; i >= 0; i--) { maxRight[i] = Math.max(maxRight[i + 1], height[i + 1]); } int sum = 0; for (int i = 1; i < height.length - 1; i++) { int min = Math.min(maxLeft[i], maxRight[i]); int sub = min - height[i]; sum += sub > 0 ? sub : 0; } return sum; }}\n无重复字符的最长子串\nhttps://leetcode.cn/problems/longest-substring-without-repeating-characters/?envType=study-plan-v2&envId=top-100-liked\n本题可以使用滑动窗口“框住”没有重复字符的子串,遍历完成后最长的窗口长度即为答案。\n怎么保证滑动窗口中没有重复字符串呢?使用哈希表存储。\n首先使用两个指针j, i为滑动窗口的边界,表示[j, i]区间的字符串都不重复。并且使用一个Set集合保存该窗口中的字符,遍历时如果待加入的字符已经存在了,则一直遍历删除窗口左端的字符直到没有重复的。\nclass Solution { public int lengthOfLongestSubstring(String s) { char[] chs = s.toCharArray(); Set<Character> set = new HashSet<>(); int result = 0; for (int i = 0, j = 0; i < chs.length; i++) { while (set.contains(chs[i])) { set.remove(chs[j++]); } set.add(chs[i]); result = Math.max(result, set.size()); } return result; }}\n和为 K 的子数组\nhttps://leetcode.cn/problems/subarray-sum-equals-k/description/?envType=study-plan-v2&envId=top-100-liked\n要求数组中的子数组的和,子数组定义为:连续非空序列。可以理解为一段区间内数值的和,这样可以想到使用前缀和来计算,前缀和数组preSum[i]表示原数组nums中第一个数一直加到第i个数的和,如果表示某一段区间[j, i]的和,计算preSum[i] - preSum[j - 1]即可。\n需要注意的是:前缀和数组的长度一般比原数组多一个,多余的一个数表示的是数组中第\n0 个数到第 0\n个数(即没有任何数的情况下)的和,同时这样做也为了编码简便。\n❓ 代码中初始时加入的(0, 1)键值对表示前缀和为 0\n的有一个(前缀和中什么数都没有),暂时还不理解。\nclass Solution { public int subarraySum(int[] nums, int k) { int n = nums.length; int[] preSum = new int[n + 1]; Arrays.fill(preSum, 0); // 计算前缀和,一般下标从1开始,0下标用于表示没有数的时候的前缀和。 for (int i = 1; i < n + 1; i++) { preSum[i] = preSum[i - 1] + nums[i - 1]; } int result = 0; Map<Integer, Integer> mp = new HashMap<>(); for (int i = 0; i < n + 1; i++) { if (mp.containsKey(preSum[i] - k)) { result += mp.get(preSum[i] - k); } mp.put(preSum[i], mp.getOrDefault(preSum[i], 0) + 1); } return result; }}\n滑动窗口最大值\nhttps://leetcode.cn/problems/sliding-window-maximum/?envType=study-plan-v2&envId=top-100-liked\n滑动窗口求最大值通常使用单调队列,但是具体实现采用双端队列。\nclass Solution { public int[] maxSlidingWindow(int[] nums, int k) { int[] resultArr = new int[nums.length - (k - 1)]; Deque<Integer> deque = new LinkedList<>(); for (int i = 0, j = 1 - k; i < nums.length; i++, j++) { // 如果被删除的值正好是之前窗口中最大的元素,则需要在队列中移除。 // 保证队列中的元素都在窗口中 if (j > 0 && deque.getFirst() == nums[j - 1]) { deque.removeFirst(); } // 从队列尾部加入元素,加入前判断队列尾部的值是否小于要加入的值,小于则删除 while (deque.size() != 0 && deque.getLast() < nums[i]) { deque.removeLast(); } deque.addLast(nums[i]); if (j >= 0) { resultArr[j] = deque.getFirst(); } } return resultArr; }}\n另一种写法,队列中保存的是窗口中元素的下标,通过下标判断元素是否在窗口中。\nclass Solution { public int[] maxSlidingWindow(int[] nums, int k) { int[] resultArr = new int[nums.length - (k - 1)]; Deque<Integer> deque = new LinkedList<>(); for (int i = 0, j = 1 - k; i < nums.length; i++, j++) { if (j > 0 && deque.getFirst() < j) { deque.removeFirst(); } while (deque.size() != 0 && nums[deque.getLast()] < nums[i]) { deque.removeLast(); } deque.addLast(i); if (j >= 0) { resultArr[j] = nums[deque.getFirst()]; } } return resultArr; }}\n最小覆盖子串\nhttps://leetcode.cn/problems/minimum-window-substring/description/?envType=study-plan-v2&envId=top-100-liked\n使用滑动窗口,窗口由两个指针控制大小,右指针负责扩大窗口,左指针负责收缩窗口。如果当前窗口不覆盖子串,则扩大窗口,如果覆盖子串,则收缩窗口更新结果,直至不能收缩。\n❓问题在于如何判断是否覆盖?\n可以使用哈希表存储窗口中每个字符出现的次数,如果目标字符串中的字符出现的次数都小于等于窗口中的,则说明覆盖,否则不覆盖。\nclass Solution { public String minWindow(String s, String t) { char[] chsArr = s.toCharArray(); char[] chtArr = t.toCharArray(); Map<Character, Integer> chCntMp = new HashMap<>(); for (char ch : chtArr) { chCntMp.put(ch, chCntMp.getOrDefault(ch, 0) + 1); } Map<Character, Integer> winMap = new HashMap<>(); int ansLeft = -1, ansRight = chsArr.length; for (int i = 0, j = 0; i < chsArr.length; i++) { winMap.put(chsArr[i], winMap.getOrDefault(chsArr[i], 0) + 1); while (isCover(winMap, chCntMp)) { if (i - j + 1 < ansRight - ansLeft + 1) { ansRight = i; ansLeft = j; } winMap.put(chsArr[j], winMap.get(chsArr[j++]) - 1); } } return ansLeft == -1 ? \"\" : s.substring(ansLeft, ansRight + 1); } public boolean isCover(Map<Character, Integer> s, Map<Character, Integer> t) { if (s.size() < t.size()) { return false; } for (Map.Entry<Character, Integer> entry : t.entrySet()) { if (s.getOrDefault(entry.getKey(), 0) < entry.getValue()) { return false; } } return true; }}\n最大子数组和\nhttps://leetcode.cn/problems/maximum-subarray/?envType=study-plan-v2&envId=top-100-liked\n利用动态规划可以达到O(N)的时间复杂度。\n假定dp[i]表示以第 i\n个数结尾的最大的子数组的和,所以递归公式:dp[i] = max(dp[i - 1] + nums[i], nums[i]),如果前一个子数组加上nums[i]数组和变小,就认为最大的数组是nums[i]自己。\nclass Solution { public int maxSubArray(int[] nums) { int ans = Integer.MIN_VALUE; for (int i = 0; i < nums.length; i++) { if (i > 0) { nums[i] = Math.max(nums[i], nums[i - 1] + nums[i]); } ans = Math.max(ans, nums[i]); } return ans; }}\n轮转数组\nhttps://leetcode.cn/problems/rotate-array/?envType=study-plan-v2&envId=top-100-liked\n主要是空间复杂度达到O(1),不适用额外的数组。\n可以采用基于交换的方式,通过观察结果,可以发现,向右移动 k\n个单位后,后边 k 个数移动到前 k 个位置,其余的移动到了后 k\n个位置。如果先采用整体翻转的方式,可以将后 k 的数移动到前 k\n个位置,但是顺序和正确的顺序相反,因此再对局部进行依次翻转即可。\n❗ 这里有一个需要注意:如果轮转的长度\nk,大于数组长度,可以采用一次取模操作,当 k\n和数组长度相等时,轮转后和原数组相同,所以可以认为 k\n取模后的结果为真实的需要轮转的长度。\nclass Solution { public void rotate(int[] nums, int k) { k %= nums.length; reverse(nums, 0, nums.length - 1); reverse(nums, 0, k - 1); reverse(nums, k, nums.length - 1); } private void reverse(int[] nums, int start, int end) { for (int i = start, j = end; i < j; i++, j--) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } }}\n缺失的第一个正数\n41.\n缺失的第一个正数 - 力扣(LeetCode)\n难点在于不能使用额外空间。如果可以使用额外空间,可以使用哈希表,存储每一个元素,然后从\n1 开始遍历,直到发现不存在于哈希表中的值即可。\n如果要降低空间复杂度,可以考虑原地哈希。\n通过观察结果,会发现,结果肯定存在于[1, n+1]中,n\n为数组长度,如果数组中的数在[1, n]都存在,则说明结果为 n + 1,否则为[1,\nn]中不存在的最小的数,这里可以使用原地哈希,具体方法可以采用标记法,遍历数组,如果\nnums[i]范围在[1, n]中,则给 nums[nums[i] -\n1]打上标记,这里打标记可以设置为负数,因为原数组中存在负数,可以令原来的负数设为一个不可能的值,如\nn + 1\n或者更大,这样所有数都是正数了。当遍历完所有打完标记,如果所有的数都是负数了,说明都存在了,否则为第一个不为负数的下标+\n1。\nclass Solution { public int firstMissingPositive(int[] nums) { for (int i = 0; i < nums.length; i++) { if (nums[i] <= 0) { nums[i] = nums.length + 1; } } for (int i = 0; i < nums.length; i++) { int temp = Math.abs(nums[i]); if (temp >= 1 && temp <= nums.length) { nums[temp - 1] = -Math.abs(nums[temp - 1]); } } for (int i = 0; i < nums.length; i++) { if (nums[i] > 0) { return i + 1; } } return nums.length + 1; }}\n旋转图像\n48.\n旋转图像 - 力扣(LeetCode)\n通过观察例子中的旋转,固定的位置之间会形成一个完整的旋转链,因此可以使用原地操作数据,不需要使用新的二维数组。\n\n所以接下来的重点是寻找旋转链之间的对应的坐标关系。以左上角的(i, j)为例,\n旋转一次后的坐标为:(j, n - i - 1),可以理解为,(i, j)所在的列变成了行,行变成了列。从行看,i行的数据会变成列,行是从上往下数,那么变成列后就是从右往左数,所以列就是n - i - 1。从列看,从左向右看,变成行就是从上往下看,因此就是j。\n(j, n - i - 1)再旋转一次后,坐标为:(n - i - 1, n - j - 1),从行看,是从上往下,旋转后的列则是从右向左,所以是n - j - 1。从列看,从右往左,旋转后的行则是从下往上,n - i - 1\n依此类推:(n - j - 1, i)\n最后的坐标旋转链(i, j) -> (j, n - i - 1) -> (n - i - 1, n - j - 1) -> (n - j - 1, i) -> (i, j)\n因此,原地操作只需要按照这个顺序依次赋值即可。\n接下来需要考虑的是,遍历哪些数据进行旋转操作。只有两种情况,n\n为奇数和偶数的情况。\nn 为偶数:只需要考虑左上角 行[0, n / 2)\n列[0, n / 2)的情况\nn\n为奇数:不需要考虑正中心的数,然后需要旋转的则是正中心左方和上方组成的区域,即\n行[0, n / 2) 列[0, n / 2]\n\nclass Solution { public void rotate(int[][] matrix) { int n = matrix.length; for (int i = 0; i < n / 2; i++) { for (int j = 0; j < (n + 1) / 2; j++) { int temp = matrix[i][j]; matrix[i][j] = matrix[n - 1 - j][i]; matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]; matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]; matrix[j][n - 1 - i] = temp; } } }}\n相交链表\n160.\n相交链表 - 力扣(LeetCode)\n判断两个链表是否相交,可以使用双指针的方式,题目中保证链表中不存在环,因此大大简化了判断条件。\n两个指针分别遍历两个链表,当遍历到末尾时再遍历另一个链表,直到指向相同的节点,即为相交的第一个节点。\n/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { return getIntersectionNode(headA, headB, null); } private ListNode getIntersectionNode(ListNode headA, ListNode headB, ListNode endNode) { if (headA == null || headB == null) { return null; } ListNode pA = headA; ListNode pB = headB; while (pA != pB) { pA = pA == endNode ? headB : pA.next; pB = pB == endNode ? headA : pB.next; } return pA; }}\n❓ 如果链表中存在环呢?则需要考虑多种情况。\n这里认为链表都是单链表\n\n情况 1:两个链表都无环双指针,遍历链表 A 和 B,当指针为 null\n时从另一个节点的头节点开始遍历。直到两个指针相等(两个指针相等时,如果为\nnull 则表示没有相交节点,否则有相交节点)面试题\n02.07. 链表相交 - 力扣(LeetCode)\n\npublic class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if (headA == null || headB == null) { return null; } ListNode pA = headA, pB = headB; while (pA != pB) { pA = pA == null ? headB : pA.next; pB = pB == null ? headA : pB.next; } return pA; }}\n❗注意:判断是否为null时,使用的是pA当前指针,而不是pA.next,如果使用pA.next来判断,当不相交时,会发生无限循环的情况,pA和pB会一直不相等(也不为null)。所以使用pA当前指针。可以理解为,把最后链表结束时指向的null指针也算作一个节点,然后两个链表不相交时,最后都会指向null节点,那么两个链表就在null节点“相交”了,如下图所示。\n\n\n情况\n2:一个有环一个无环(一定不相交)如果相交,则肯定有一个节点有两个next指针,这不满足单链表。所以一定不相交。\n情况 3:两个都有环\n\n不相交\n在非环处相交\n在环处相交\n\n\n相交只有两种情况\n\n找到两个带环链表的入环节点,然后固定一个,遍历另一个,直到能找到一个节点和固定节点相等,则证明相交,否则不相交。\n// circleNode1 circleNode2是两个节点的入环节点// 如果入环节点相同,说明必然是情况1。否则是情况2或者不相交if (circleNode1 == circleNode2) { // 利用circleNode1或者2为末尾节点,利用无环链表求相交节点方式求相交的节点。 return true;}Node temp = circleNode2.next;while(temp != circleNode2) { if(temp == circleNode1) // circleNode1 或者 circleNode2为相交节点都可以 return true; temp = temp.next;}// 不相交return false;\n所以所有情况如下:\n\npublic static ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode cycleNodeA = hasCycle(headA); ListNode cycleNodeB = hasCycle(headB); if (cycleNodeA == cycleNodeB) { // 说明两个都是无环的,直接判断是否相交 // if (cycleNodeA == null) { // return getIntersectionNodeNoLoop(headA, headB, null); // } // 否则是都有环,入环节点相同,必然相交,以入环节点为终止节点求相交节点 // return getIntersectionNodeNoLoop(headA, headB, cycleNodeA); // 以上代码可以直接合并为下面一行 return getIntersectionNodeNoLoop(headA, headB, cycleNodeA); } // 说明两个入环节点不相等,要么有一个为空,要么都不为空,如果有一个为空,则表示肯定不相交 if (cycleNodeA == null || cycleNodeB == null) { return null; } // 如果环上相交,说明从一个入环节点开始遍历,一定能到达另一个入环节点 ListNode temp = cycleNodeA; while (temp != cycleNodeB) { temp = temp.next; // 转了一圈发现回到原位置了,说明没有相交 if (temp == cycleNodeA) { return null; } } // 相交,任意一个入环节点都可以是相交节点。 return cycleNodeA; // 如果入环节点不同,并且相交,那么肯定有两个相交节点 // return cycleNodeB;}// 判断一个链表是否有环public static ListNode hasCycle(ListNode head) { if (head == null) { return null; } ListNode slow = head, fast = head; while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; if (fast == slow) { fast = head; while (fast != slow) { fast = fast.next; slow = slow.next; } return fast; } } return null;}// 判断两个无环链表是否相交,手动设定终止节点endNodepublic static ListNode getIntersectionNodeNoLoop(ListNode headA, ListNode headB, ListNode endNode) { ListNode pA = headA, pB = headB; while (pA != pB) { pA = pA == endNode ? headB : pA.next; pB = pB == endNode ? headA : pB.next; } return pA;}\n反转链表\n206.\n反转链表 - 力扣(LeetCode)\n使用三个指针分别指向“上一个节点”“当前节点”和“下一个节点”,遍历过程中反转“上一个节点”和“当前节点”的指向。\n/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode reverseList(ListNode head) { ListNode pre = null, cur = head; while (cur != null) { ListNode next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre; }}\n回文链表\n234.\n回文链表 - 力扣(LeetCode)\n需要使用O(1)的空间复杂度,回文的定义:从前向后和从后向前遍历的结果相同。但是原链表是单向链表,因此如果想从后向前遍历,需要将后半部分的链表反转。\n因此需要判断什么时候到达了“中间”位置,然后将中间及其之后的链表反转。\n这里使用快慢指针,因为快指针行进速度是慢指针 2\n倍,所以当快指针到结尾时,慢指针行进了链表长度的一半。\n这里分两种情况考虑,链表长度为奇数和偶数。\n\n如图所示,当快慢指针结束时,奇数长度的链表慢指针在正中心的位置,偶数长度的链表慢指针在前半部分的最后一个节点位置。\n通过观察可以知道,奇数长度的链表正中心的节点并不影响整体的回文性,偶数长度的链表需要将后半部分的链表反转,因此可以确定需要反转的链表的头节点为slow.next,反转完后返回右半部分头节点,从左半部分头节点和右半部分头节点同时遍历,如果不同则直接返回false,需要注意终止条件是右半部分的节点走到null,因为链表长度为奇数时,左半部分的链表会多一个中心节点。\n/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public boolean isPalindrome(ListNode head) { if (head == null) { return true; } ListNode fast = head, slow = head; while (fast.next != null && fast.next.next != null) { fast = fast.next.next; slow = slow.next; } fast = reverse(slow.next); // 保存反转后的链表的头节点 ListNode temp = fast; slow = head; while (fast != null) { if (fast.val != slow.val) { return false; } fast = fast.next; slow = slow.next; } // 链表恢复原状 reverse(temp); return true; } private ListNode reverse(ListNode head) { ListNode pre = null, cur = head; while (cur != null) { ListNode next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre; }}\n环形链表\n142.\n环形链表 II - 力扣(LeetCode)\n判断链表是否有环,使用快慢指针,当快慢指针相遇时表明存在环,然后将快指针从头节点开始遍历,每次移动一个位置,直到再次相遇的节点即为环形的入口。\n❓ 为什么将快指针从头节点开始遍历就能找到入口?下边给出证明:\n\n假设环形链表中三个点,头节点 A,入口节点 B,快慢指针相遇的节点\nC。因为快指针的速度是慢指针的 2 倍,所以快指针行进的距离是慢指针的 2\n倍,所以有,移项可得即,也就是说从 C 点和 A 点出发,以同样的速度行进,会同时到达\nB 即入口节点。\n/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */public class Solution { public ListNode detectCycle(ListNode head) { if (head == null) { return null; } ListNode slow = head, fast = head; while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { fast = head; while (slow != fast) { slow = slow.next; fast = fast.next; } return fast; } } return null; }}\n删除链表的倒数第 N 个节点\n19.\n删除链表的倒数第 N 个结点 - 力扣(LeetCode)\n要求使用一趟遍历,难点在于遍历一趟就要找到第 N\n个节点所在的位置。如果只用一个指针遍历长度,则需要遍历两次。\n如果使用两个指针,让第一个指针先走n - 1步,然后让第二个指针和第一个指针同时行进,则可以保证两个指针的距离固定,当第一个指针到达结尾时,第二个指针的位置则为需要删除的节点,然后进行删除即可。(注意:需要多一个pre指针,不然无法删除)\n/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode first = head; ListNode second = head; ListNode pre = null; for (int i = 0; i < n - 1; i++) { first = first.next; } while (first.next != null) { first = first.next; pre = second; second = second.next; } if (pre == null) { return head.next; } pre.next = second.next; return head; }}\n两两交换链表中的节点\n24.\n两两交换链表中的节点 - 力扣(LeetCode)\n采用“类似头插法”方式,设置一个temp节点,temp节点指向的后续的节点即为需要交换的节点。\n/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode swapPairs(ListNode head) { if (head == null || head.next == null) { return head; } ListNode ansHead = new ListNode(); // 保证temp的后续节点存在 ansHead.next = head; ListNode temp = ansHead; // 保证后续节点存在两个节点才可以交换,否则不能交换 while (temp.next != null && temp.next.next != null) { ListNode node1 = temp.next; ListNode node2 = temp.next.next; temp.next = node2; // 保证链表使用是连接的,没有断开 node1.next = node2.next; node2.next = node1; // 进行xi temp = node1; } return ansHead.next; }}\nK 个一组翻转链表\n25.\nK 个一组翻转链表 - 力扣(LeetCode)\n要求 K 个一组,可以参考删除链表的倒数第 N\n个节点,先走 K 步,然后翻转这 K 个链表,依此类推。\n需要注意的是设置一个dummy节点,即带头节点,方便后续操作。\n\n/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } *//** * @param {ListNode} head * @param {number} k * @return {ListNode} */var reverseKGroup = function (head, k) { let dummy = new ListNode(0, head); let pre = dummy, cur = dummy; while (cur.next !== null) { for (let i = 0; i < k && cur; i++) { cur = cur.next; } if (!cur) { break; } let start = pre.next, end = cur.next; pre.next = reverse(start, end); start.next = end; pre = start; cur = start; } return dummy.next;};/** head为头节点,end为最后一个节点的下一个节点(理解为null)*/function reverse(head, end) { if (!head) { return null; } let pre = null, cur = head; while (cur !== end) { let next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre;}\n随机链表的复制\n138.\n随机链表的复制 - 力扣(LeetCode)\n方法一:哈希表,存储每个节点的对应复制的节点,然后遍历添加指针。\n方法二:\n构造新链表旧链表1 -> 新链表1 -> 旧链表2 -> 新链表2 -> ... -> 旧链表n -> 新链表n,然后遍历,通过旧链表的random指针,找到新链表的random节点。\n/** * // Definition for a _Node. * function _Node(val, next, random) { * this.val = val; * this.next = next; * this.random = random; * }; *//** * @param {_Node} head * @return {_Node} */var copyRandomList = function (head) { if (!head) { return null; } // 复制链表 let temp = head; while (temp) { let node = new _Node(temp.val, temp.next, null); temp.next = node; temp = node.next; } // 连接新节点的random指针 temp = head; while (temp) { if (temp.random) { temp.next.random = temp.random.next; } temp = temp.next.next; } // 拆分链表 temp = head; let ansHead = head.next; let ansTemp = ansHead; while (temp) { temp.next = ansTemp.next; temp = ansTemp.next; if (temp) { ansTemp.next = temp.next; ansTemp = temp.next; } } return ansHead;};\n排序链表\n148.\n排序链表 - 力扣(LeetCode)\n链表排序,通常使用归并排序。\n利用快慢指针找到中点,然后两边分别排序、归并。\n注意的是 ?? 和 || 的区别:\n\nx ?? y是当x为null、undefined时取\ny,如果x为false、0、''这种仍然取x。\nx || y当x为假值即null、undefined、false、0、''这些值时取y\n\n/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } *//** * @param {ListNode} head * @return {ListNode} */var sortList = function (head) { if (!head || !head.next) { return head; } let fast = head, slow = head; while (fast.next && fast.next.next) { fast = fast.next.next; slow = slow.next; } let mid = slow.next; slow.next = null; let left = sortList(head); let right = sortList(mid); let ansHead = new ListNode(); let tail = ansHead; while (left && right) { if (left.val < right.val) { tail.next = left; left = left.next; } else { tail.next = right; right = right.next; } tail = tail.next; } tail.next = left ?? right; return ansHead.next;};\n合并 K 个升序链表\n23.\n合并 K 个升序链表 - 力扣(LeetCode)\n归并排序的思想,每两个合成一个,然后继续合成,直到剩下最后一个。\n/** * Definition for singly-linked list. * function ListNode(val, next) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } *//** * @param {ListNode[]} lists * @return {ListNode} */var mergeKLists = function (lists) { if (lists.length === 0) { return null; } return mergeList(lists, 0, lists.length - 1);};function mergeList(lists, left, right) { if (left >= right) { return lists[left]; } let mid = (left + right) >> 1; let leftList = mergeList(lists, left, mid); let rightList = mergeList(lists, mid + 1, right); let dummy = new ListNode(); let tail = dummy; while (leftList && rightList) { if (leftList.val < rightList.val) { tail.next = leftList; leftList = leftList.next; } else { tail.next = rightList; rightList = rightList.next; } tail = tail.next; } tail.next = leftList ?? rightList; return dummy.next;}\n实现 LRU 缓存\n146.\nLRU 缓存 - 力扣(LeetCode)\n最近最少使用缓存,可以使用一个链表保存数据,如果被使用了则放在链表头或者尾,这样一来,链表的某个方向上就是最近的使用频率递减的趋势。同时使用链表移动数据的操作是\nO(1)的。\n但是查找很耗时,因此可以采用哈希的方式,存储每个 key\n对应的链表的节点。\nclass ListNode { constructor(val, pre, next) { this.val = val ?? { key: 0, value: 0 }; this.pre = pre ?? null; this.next = next ?? null; }}/** * @param {number} capacity */var LRUCache = function (capacity) { this.capacity = capacity; this.size = 0; this.dummyHead = new ListNode(); this.dummyTail = new ListNode(); this.dummyHead.next = this.dummyTail; this.dummyTail.pre = this.dummyHead; this.kv = new Map();};/** * @param {number} key * @return {number} */LRUCache.prototype.get = function (key) { if (!this.kv.has(key)) { return -1; } const temp = this.kv.get(key); removeNode(temp); addToTail(this.dummyTail, temp); return temp.val.value;};/** * @param {number} key * @param {number} value * @return {void} */LRUCache.prototype.put = function (key, value) { if (this.kv.has(key)) { const temp = this.kv.get(key); temp.val.value = value; removeNode(temp); addToTail(this.dummyTail, temp); } else { const temp = new ListNode({ key, value }); addToTail(this.dummyTail, temp); this.kv.set(key, temp); if (this.size < this.capacity) { this.size++; } else { this.kv.delete(this.dummyHead.next.val.key); removeNode(this.dummyHead.next); } }};function removeNode(temp) { temp.pre.next = temp.next; temp.next.pre = temp.pre;}function addToTail(dummyTail, temp) { temp.next = dummyTail; temp.pre = dummyTail.pre; dummyTail.pre = temp; temp.pre.next = temp;}/** * Your LRUCache object will be instantiated and called as such: * var obj = new LRUCache(capacity) * var param_1 = obj.get(key) * obj.put(key,value) */\nstruct Node { int key, val; Node *next; Node *prev; Node(int key, int val) { this->key = key; this->val = val; this->next = nullptr; this->prev = nullptr; } Node(int key, int val, Node *next, Node *prev) { this->key = key; this->val = val; this->next = next; this->prev = prev; }};class LRUCache {public: int size; unordered_map<int, Node*> hash; Node *dummyHead; Node *dummyTail; LRUCache(int capacity) { size = capacity; dummyHead = new Node(0, 0); dummyTail = new Node(0, 0); dummyHead->next = dummyTail; dummyTail->prev = dummyHead; } void addNodeToHead(Node *temp) { temp->next = dummyHead->next; temp->prev = dummyHead; dummyHead->next->prev = temp; dummyHead->next = temp; } void removeNode(Node *temp) { temp->prev->next = temp->next; temp->next->prev = temp->prev; } int get(int key) { if (!hash.count(key)) { return -1; } auto temp = hash[key]; removeNode(temp); addNodeToHead(temp); return temp->val; } void put(int key, int value) { if (hash.count(key)) { auto temp = hash[key]; temp->val = value; removeNode(temp); addNodeToHead(temp); } else { auto temp = new Node(key, value); hash[key] = temp; addNodeToHead(temp); if (hash.size() > size) { auto removed = dummyTail->prev; removeNode(removed); hash.erase(removed->key); delete removed; } } }};/** * Your LRUCache object will be instantiated and called as such: * LRUCache* obj = new LRUCache(capacity); * int param_1 = obj->get(key); * obj->put(key,value); */\n二叉树的层序遍历\n102.\n二叉树的层序遍历 - 力扣(LeetCode)\n层序遍历使用队列即可,但是题目中要求每一层的数据单独作为一个子数组保存。\n在每次遍历队列之前,可以获取队列的长度,即表示当前层的节点个数。然后一次性遍历当前层所有的节点,下一次遍历的时候,就是遍历的下一层的节点。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {TreeNode} root * @return {number[][]} */var levelOrder = function (root) { if (!root) { return []; } const ans = []; const que = [root]; while (que.length) { const ret = []; const size = que.length; for (let i = 0; i < size; i++) { const temp = que.shift(); ret.push(temp.val); if (temp.left) { que.push(temp.left); } if (temp.right) { que.push(temp.right); } } ans.push(ret); } return ans;};\n将有序数组转为平衡二叉搜索树\n108.\n将有序数组转换为二叉搜索树 - 力扣(LeetCode)\n要求为平衡二叉树,利用二分查找得到的搜索树是平衡的,可以利用二分查找的思路构造。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {number[]} nums * @return {TreeNode} */var sortedArrayToBST = function (nums) { return recur(nums, 0, nums.length - 1);};function recur(nums, left, right) { if (left === right) { return new TreeNode(nums[left]); } else if (left > right) { return null; } const mid = (left + right) >> 1; const root = new TreeNode(nums[mid]); root.left = recur(nums, left, mid - 1); root.right = recur(nums, mid + 1, right); return root;}\n验证是否是二叉搜索树\n98.\n验证二叉搜索树 - 力扣(LeetCode)\n利用二叉搜索树的性质,中序遍历的结果是有序的。因此可以采用中序遍历,同时使用一个变量保存遍历时上次的结果,当前值大于上一次的值并且左右子树都为二叉搜索树才为二叉搜索树。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {TreeNode} root * @return {boolean} */var isValidBST = function (root) { let pre = Number.MIN_SAFE_INTEGER; function recur(root) { if (!root) { return true; } const leftBST = recur(root.left); const cur = root.val; if (pre < cur) { pre = cur; } else { return false; } const rightBST = recur(root.right); return leftBST && rightBST; } return recur(root);};\n二叉搜索树第 K 小的元素\n230.\n二叉搜索树中第 K 小的元素 - 力扣(LeetCode)\n由二叉搜索树的性质,中序遍历为从小到大的序列,可以采用中序遍历的方式,遍历到第\nK 个数的时候记录值,即为第 K 小的元素。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {TreeNode} root * @param {number} k * @return {number} */var kthSmallest = function (root, k) { let ans = 0; function recur(root) { if (!root) { return; } recur(root.left); // 遍历一个之后k减1,当k === 0时说明已经遍历到了,后续就不能再遍历了。 if (k === 0) { return; } if (k === 1) { ans = root.val; } k--; recur(root.right); } recur(root); return ans;};\n二叉树的右视图\n199.\n二叉树的右视图 - 力扣(LeetCode)\n要求每一层的最右边的数据,可以考虑用层序遍历,每一层的最后一个数据为结果集的数据。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {TreeNode} root * @return {number[]} */var rightSideView = function (root) { if (!root) { return []; } const ans = []; const que = []; que.push(root); while (que.length) { const size = que.length; for (let i = 0; i < size; i++) { const temp = que.shift(); if (i === size - 1) { ans.push(temp.val); } // 下边两个判断可以用另一种方式写 // temp.left && que.push(temp.left) // temp.right && que.push(temp.right) if (temp.left) { que.push(temp.left); } if (temp.right) { que.push(temp.right); } } } return ans;};\n二叉树展开为链表\n114.\n二叉树展开为链表 - 力扣(LeetCode)\n按照先序遍历方式展开,左指针始终为\nnull,而元素都在右指针,考虑按照递归的做法,将左右子树分别展开,然后将左子树的节点(如果有)链接到右子树上。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {TreeNode} root * @return {void} Do not return anything, modify root in-place instead. */var flatten = function (root) { if (!root) { return null; } flatten(root.left); flatten(root.right); let temp = root.left; if (temp) { // 一直找到展开好的左子树的最后的节点 while (temp.right) { temp = temp.right; } temp.right = root.right; root.right = root.left; root.left = null; }};\n要求使用O(1)的空间复杂度,不使用前序遍历的方式,转换思路。\n前序遍历特点为:根、左、右,如果要展开成一个链表,则当前节点curr的右子节点,一定排在curr的左子树的某个节点之后。根据特点,可以知道,左子树的最后一个节点一定是左子树(非空)最右侧的节点,因此找到后,可以认为这个最右侧的节点是当前节点的右子节点的前驱结点prev,将prev的右孩子设置为当前节点的右子节点,将当前节点的左子树设置为右孩子(因为左子树一定是当前节点的后驱节点),并将左子树置空。将当前节点向右孩子走一步,完成一轮循环。\n/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */class Solution {public: void flatten(TreeNode* root) { if (!root) return; auto curr = root; while (curr) { if (curr->left) { auto prev = curr->left; while (prev->right) { prev = prev->right; } prev->right = curr->right; curr->right = curr->left; curr->left = nullptr; } curr = curr->right; } }};\n从前序遍历和中序遍历构造二叉树\n105.\n从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)\n根据前序遍历特点,最先遍历的肯定是根节点,所以第一个数一定是根节点,然后根据这个根节点,从中序遍历找到对应的位置,那么中序遍历该位置的左边为左子树的节点,右边为右子树的节点,依次递归。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {number[]} preorder * @param {number[]} inorder * @return {TreeNode} */var buildTree = function (preorder, inorder) { function recur(preorder, preLeft, preRight, inorder, inLeft, inRight) { if (preLeft > preRight) { return null; } const root = new TreeNode(preorder[preLeft]); const inIndex = inorder.findIndex((item) => item === root.val); const leftLength = inIndex - inLeft, rightLength = inRight - inIndex; root.left = recur( preorder, preLeft + 1, preLeft + leftLength, inorder, inLeft, inIndex - 1 ); root.right = recur( preorder, preLeft + leftLength + 1, preRight, inorder, inIndex + 1, inRight ); return root; } return recur(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);};\n路径总和 Ⅲ\n437.\n路径总和 III - 力扣(LeetCode)\n法一:深度优先搜索,每次计算以root为根节点,向下计算满足targetSum的个数,记为rootSum(root, targetSum),然后递归遍历每个节点,计算以每个节点为根节点的满足条件的个数。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {TreeNode} root * @param {number} targetSum * @return {number} */var pathSum = function (root, targetSum) { if (!root) { return 0; } return ( rootSum(root, targetSum) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum) );};function rootSum(root, targetSum) { if (!root) { return 0; } return ( rootSum(root.left, targetSum - root.val) + rootSum(root.right, targetSum - root.val) + (root.val === targetSum ? 1 : 0) );}\n法二:前缀和\n从root节点到node节点的路径上的和记为前缀和,可以看作是一维的前缀和。\n使用前序遍历,遍历过程中记录前缀和,同时记录满足该前缀和的节点个数。\n如果root到node之间的节点p满足prefixSum - targetSum,则说明p节点的下一个节点到node节点前缀和为targetSum。\n计算完成后恢复,因为计算其他路径的前缀和时可能有相同的前缀和,但是其他路径的会影响当前路径,因此需要删除。参考:题解\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {TreeNode} root * @param {number} targetSum * @return {number} */var pathSum = function (root, targetSum) { if (!root) { return 0; } const prefix = new Map(); prefix.set(0, 1); return dfs(root, prefix, 0, targetSum);};function dfs(root, prefix, curr, targetSum) { if (!root) { return 0; } curr += root.val; let ret = prefix.get(curr - targetSum) ?? 0; prefix.set(curr, (prefix.get(curr) ?? 0) + 1); ret += dfs(root.left, prefix, curr, targetSum); ret += dfs(root.right, prefix, curr, targetSum); prefix.set(curr, prefix.get(curr) - 1); return ret;}\n二叉树的最近公共祖先\n236.\n二叉树的最近公共祖先 - 力扣(LeetCode)\n/** * Definition for a binary tree node. * function TreeNode(val) { * this.val = val; * this.left = this.right = null; * } *//** * @param {TreeNode} root * @param {TreeNode} p * @param {TreeNode} q * @return {TreeNode} */var lowestCommonAncestor = function (root, p, q) { if (!root || root === p || root === q) { return root; } const left = lowestCommonAncestor(root.left, p, q); const right = lowestCommonAncestor(root.right, p, q); if (!left) { return right; } if (!right) { return left; } return root;};\n二叉树中的最大路径和\n124.\n二叉树中的最大路径和 - 力扣(LeetCode)\n采用递归的方式,设置maxGain(root)函数,表示从从root节点上能获得的最大贡献值,贡献值表示为从root节点向左子树或者右子树延申(只能是root的左子树或者右子树一个方向),得到的最大的值,以此类推,roo.left的最大贡献值表示为root.left向左子树或者右子树延申。这是一个递归的过程。\n可以在递归的时候求出最大的路径和。当左子树和右子树的最大贡献值大于 0\n的时候,才能算作贡献去计算路径和,否则还不如直接使用根节点当作单独的路径和大。最后返回贡献值,选择左右子树最大的作为贡献值。\n/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } *//** * @param {TreeNode} root * @return {number} */var maxPathSum = function (root) { let maxSum = -Infinity; function maxGain(root) { if (!root) { return 0; } const leftGain = Math.max(maxGain(root.left), 0); const rightGain = Math.max(maxGain(root.right), 0); const temp = root.val + leftGain + rightGain; console.log(maxSum); maxSum = Math.max(maxSum, temp); return root.val + Math.max(leftGain, rightGain); } maxGain(root); return maxSum;};\n岛屿数量\n200.\n岛屿数量 - 力扣(LeetCode)\n使用深度优先遍历,遇到1后进行深度优先遍历,将遍历到的所有1进行标记,组成岛屿。\n/** * @param {character[][]} grid * @return {number} */var numIslands = function (grid) { const m = grid.length, n = grid[0].length; const visited = new Array(m).fill(null).map((item) => new Array(n).fill(false)); const dx = [0, 1, 0, -1], dy = [1, 0, -1, 0]; function dfs(grid, x, y) { visited[x][y] = true; for (let i = 0; i < dx.length; i++) { const nx = x + dx[i], ny = y + dy[i]; if ( nx < 0 || ny < 0 || nx >= m || ny >= n || grid[nx][ny] === '0' || visited[nx][ny] ) { continue; } dfs(grid, nx, ny); } } let ans = 0; for (let i = 0; i < grid.length; i++) { for (let j = 0; j < grid[i].length; j++) { // 遍历每一个元素,当为1且没有被访问时,说明会组成一个新的岛屿,结果+1 if (grid[i][j] === '1' && !visited[i][j]) { dfs(grid, i, j); ans++; } } } return ans;};\n腐烂的橘子\n994.\n腐烂的橘子 - 力扣(LeetCode)\n多源的宽度优先搜索,因为可能同时存在多个腐烂的橘子,是同时扩散的。\n初始时找到所有腐烂的橘子加入队列,作为初始的多个起点,然后遍历,同时这里使用了102.\n二叉树的层序遍历 - 力扣(LeetCode)的方法,每一次遍历“一层”。\n/** * @param {number[][]} grid * @return {number} */var orangesRotting = function (grid) { const m = grid.length, n = grid[0].length; const que = []; // 保存所有好的橘子 let cnt = 0; for (let i = 0; i < grid.length; i++) { for (let j = 0; j < grid[i].length; j++) { grid[i][j] === 2 && que.push([i, j]); grid[i][j] === 1 && cnt++; } } const dx = [0, 1, 0, -1], dy = [1, 0, -1, 0]; let ans = 0; // 没有遍历完并且还有好的橘子,说明可以继续腐烂,否则,不再继续遍历。 while (que.length && cnt) { ans++; let size = que.length; while (size--) { const [x, y] = que.shift(); for (let i = 0; i < dx.length; i++) { const nx = x + dx[i], ny = y + dy[i]; if (nx < 0 || ny < 0 || nx >= m || ny >= n || grid[nx][ny] !== 1) { continue; } // 腐烂后则减1 cnt--; grid[nx][ny] = 2; que.push([nx, ny]); } } } // 如果还有好的橘子,说明不可能 return cnt === 0 ? ans : -1;};\n课程表\n207.\n课程表 - 力扣(LeetCode)\n拓扑排序,首先是图的构造,使用二维数组表示,graph[i] = [a, b, c]表示i指向a, b, c。\n/** * @param {number} numCourses * @param {number[][]} prerequisites * @return {boolean} */var canFinish = function (numCourses, prerequisites) { const graph = new Array(numCourses).fill(null).map((item) => []); const indegree = new Array(numCourses).fill(0); for (const [a, b] of prerequisites) { graph[b].push(a); indegree[a]++; } const que = []; for (let i = 0; i < numCourses; i++) { indegree[i] === 0 && que.push(i); } while (que.length) { const i = que.shift(); for (const j of graph[i]) { indegree[j]--; indegree[j] === 0 && que.push(j); } } for (let i = 0; i < numCourses; i++) { if (indegree[i] !== 0) { return false; } } return true;};\n实现 Trie 树\n208.\n实现 Trie (前缀树) - 力扣(LeetCode)\n每个节点(假设)有 26\n个子树(根据字符集决定),每个边表示一个字符,如果有这个边,表示存在,没有边表示不存在。\n如果需要判断每个单词出现了多少次,或者是以某个单词为前缀的单词有多少个,可以增加变量\nend: int, pass: int用数量表示\nvar Trie = function () { this.next = new Map(); this.end = false;};/** * @param {string} word * @return {void} */Trie.prototype.insert = function (word) { let temp = this; for (let i = 0; i < word.length; i++) { const c = word[i]; // 原来不存在才创建,否则会丢失原有的数据 if (!temp.next.get(c)) { temp.next.set(c, new Trie()); } temp = temp.next.get(c); } temp.end = true;};/** * @param {string} word * @return {boolean} */Trie.prototype.search = function (word) { let temp = this; for (let c of word) { // 获取下一个节点 const node = temp.next.get(c); // 如果没有则说明没有添加过 if (!node) { return false; } temp = node; } // 如果end为true说明添加的是这个单词,否则只能说明以这个单词为前缀 return temp.end;};/** * @param {string} prefix * @return {boolean} */Trie.prototype.startsWith = function (prefix) { let temp = this; for (let c of prefix) { const node = temp.next.get(c); if (!node) { return false; } temp = node; } return true;};/** * Your Trie object will be instantiated and called as such: * var obj = new Trie() * obj.insert(word) * var param_2 = obj.search(word) * var param_3 = obj.startsWith(prefix) */\n全排列\n46.\n全排列 - 力扣(LeetCode)\n题目中限定了没有重复的元素,递归时无需特判。\n/** * @param {number[]} nums * @return {number[][]} */var permute = function (nums) { const ans = []; function dfs(nums, k) { if (nums.length === k) { ans.push(nums.slice()); return; } for (let i = k; i < nums.length; i++) { // 交换当前位置和第i个位置的元素 [nums[i], nums[k]] = [nums[k], nums[i]]; dfs(nums, k + 1); // 复位 [nums[i], nums[k]] = [nums[k], nums[i]]; } } dfs(nums, 0); return ans;};\n子集\n78.\n子集 - 力扣(LeetCode)\n求子集,核心思想可以用二进制考虑,每一个元素都有“要”或者“不要”两种选择,可以采用递归的方式,也可以采用迭代使用二进制方式,二进制的某位为\n1 时表示要,0 表示不要。\n使用回溯方式\n/** * @param {number[]} nums * @return {number[][]} */var subsets = function (nums) { const ans = []; function dfs(k, ret) { if (k === nums.length) { ans.push(ret.slice()); return; } dfs(k + 1, ret); ret.push(nums[k]); dfs(k + 1, ret); // 归位, ret.pop(); } dfs(0, []); return ans;};\n使用二进制方式\n/** * @param {number[]} nums * @return {number[][]} */var subsets = function (nums) { const ans = []; for (let i = 0; i < 1 << nums.length; i++) { const ret = []; for (let j = 0; j < nums.length; j++) { if (i & (1 << j)) { ret.push(nums[j]); } } ans.push(ret); } return ans;};\n电话号码的字母组合\n17.\n电话号码的字母组合 - 力扣(LeetCode)\n也是一种组合问题,但是较为简单,给定一个一组数字字符串,按照这个顺序打出来的字母组合有哪些,因为数字顺序时固定的,可以每次从当前数字中挑一个对应的字母添加到结果末尾,然后选择下一个数字的字母,依次类推。\n/** * @param {string} digits * @return {string[]} */var letterCombinations = function (digits) { if (digits.length === 0) { return []; } const numDigit = { 2: ['a', 'b', 'c'], 3: ['d', 'e', 'f'], 4: ['g', 'h', 'i'], 5: ['j', 'k', 'l'], 6: ['m', 'n', 'o'], 7: ['p', 'q', 'r', 's'], 8: ['t', 'u', 'v'], 9: ['w', 'x', 'y', 'z'], }; const ans = []; function dfs(k, ret) { if (k === digits.length) { ans.push(ret); return; } for (let c of numDigit[digits[k]]) { dfs(k + 1, ret.concat(c)); } } dfs(0, ''); return ans;};\n组合总和\n39.\n组合总和 - 力扣(LeetCode)\n难点在于结果集不能重复,如[2, 2, 3]和[2, 3, 2]是重复的。因此在\ndfs\n时需要主动避免产生重复的结果。做法:每次搜索时设置下一次搜索的起点,避免后续的搜索会搜索前边搜索过的结果。\n具体可参考:39.\n组合总和 - 力扣(LeetCode)\n/** * @param {number[]} candidates * @param {number} target * @return {number[][]} */var combinationSum = function (candidates, target) { const ans = []; // idx表示这一轮搜索的起始位置 function dfs(ret, sum, idx) { if (idx === candidates.length) { return; } if (sum === target) { ans.push(ret.slice()); return; } // 此轮搜索不要当前值,则下一轮搜索需要从下一个位置开始 dfs(ret, sum, idx + 1); if (sum + candidates[idx] <= target) { ret.push(candidates[idx]); // 此轮搜索要当前值,下一轮搜索也从当前位置开始 dfs(ret, sum + candidates[idx], idx); ret.pop(); } } dfs([], 0, 0); return ans;};\n括号生成\n22.\n括号生成 - 力扣(LeetCode)\n多种方法可以做,这里先采取回溯法做。\n回溯的前提是使用深度优先搜索,然后使用剪枝策略优化。因此首先想到使用深搜搜索出所有的结果。画出递归树:\n\n通过递归树可以看到,有些结果是不能要的,比如((((、))))等等,因此,在进行深搜时需要加上判断条件进行剪枝。\n如果括号能匹配,首先左括号的个数不能超过n,因为这里是从前往后追加括号,因此左括号的个数优先增大,而且,如果想要后续的右括号可以匹配上左括号,左括号的个数一定要大于或等于右括号,否则像(())),后续无论怎么追加括号,都不是合法的。\n/** * @param {number} n * @return {string[]} */var generateParenthesis = function (n) { const ans = []; const path = []; function dfs(left, right) { if (left > n || left < right) { return; } if (path.length === 2 * n) { ans.push(path.join('')); return; } path.push('('); dfs(left + 1, right); path.pop(); path.push(')'); dfs(left, right + 1); path.pop(); } dfs(0, 0); return ans;};\n单词搜索\n79.\n单词搜索 - 力扣(LeetCode)\n此题需要用回溯才能达到最佳,因此要考虑剪枝策略。\n首先按照深搜的方式,从某个点出发,遍历所有方向所有长度的字符串,直到找到符合条件的。\n可以想到一个剪枝策略,当下一个字符和要查找的字符串的下一个字符匹配时才进行搜索,如果不匹配,即使搜索了,也是不满足的。\n还有一个注意点,停止条件是:遍历到word字符串的最后一个位置时就要终止递归了。\n因为dfs(i, j, idx)表示的是:board[i, j]位置的字符和word[idx]位置的字符相同,只有当相同的时候才会走进这个递归。\n/** * @param {character[][]} board * @param {string} word * @return {boolean} */var exist = function (board, word) { const m = board.length, n = board[0].length; const visited = new Array(m).fill(null).map((item) => new Array(n).fill(false)); const dx = [0, 1, 0, -1], dy = [1, 0, -1, 0]; let ans = false; function dfs(x, y, idx) { if (idx === word.length - 1) { ans = true; return; } if (ans) { return; } visited[x][y] = true; for (let i = 0; i < 4; i++) { const nx = x + dx[i], ny = y + dy[i]; if ( nx >= 0 && nx < m && ny >= 0 && ny < n && !visited[nx][ny] && board[nx][ny] == word[idx + 1] ) { visited[nx][ny] = true; dfs(nx, ny, idx + 1); visited[nx][ny] = false; } } visited[x][y] = false; } for (let i = 0; i < board.length; i++) { for (let j = 0; j < board[i].length; j++) { board[i][j] === word[0] && dfs(i, j, 0); } } return ans;};\n分割回文串\n131.\n分割回文串 - 力扣(LeetCode)\n用到了两种算法,一种是动态规划,一种是回溯。\n回溯用于搜索到所有可能的子串,然后通过动态规划判断是否可以组成回文串。\n使用一个下标i表示当前搜索到的位置,[0, i - 1]表示已经搜索过的,[i, n]表示没有搜索的,然后对[i, j]进行判断,如果可以构成回文串,则从j + 1开始进行下一次搜索。\n动态规划主要用于快速判断是否是回文串,这里采用记忆化搜索的方式。\n/** * @param {string} s * @return {string[][]} */var partition = function (s) { const ans = []; const f = new Array(s.length).fill(null).map((item) => new Array(s.length).fill(null)); function dfs(i, ret) { if (i === s.length) { ans.push(ret.slice()); return; } for (let j = i; j < s.length; j++) { if (dp(i, j)) { ret.push(s.slice(i, j + 1)); dfs(j + 1, ret); ret.pop(); } } } // 记忆化搜索的方式 function dp(x, y) { if (f[x][y] !== null) { return f[x][y]; } for (let i = x, j = y; i < j; i++, j--) { if (s[i] !== s[j]) { f[x][y] = false; return false; } } f[x][y] = true; return true; } dfs(0, []); return ans;};\nN 皇后\n51.\nN 皇后 - 力扣(LeetCode)\n难点在于怎么快速判断当前位置是否是可放的位置,即判断当前位置的行、列和两个对角线是否有其他皇后。\n其中表示两个对角线是最难的。\n可以将行和列看作是坐标系,将对角线平移的时候,和y轴的交点即为在数组中的下标,需要注意的是另一种情况下标会出现负数,因此可以将整体加上n - 1保证为正数,或者也可以使用哈希表存储不用考虑正负问题。\n/** * @param {number} n * @return {string[][]} */var solveNQueens = function (n) { const row = new Array(n).fill(false), col = row.slice(); const diag = new Array(2 * n - 1), rediag = diag.slice(); const ans = []; const place = new Array(n).fill(null).map((item) => new Array(n).fill('.')); function dfs(idx) { if (idx === n) { ans.push(place.map((item) => item.join(''))); return; } for (let i = 0; i < n; i++) { if (check(idx, i)) { row[idx] = col[i] = diag[idx + i] = rediag[idx - i + n - 1] = true; place[idx][i] = 'Q'; dfs(idx + 1); place[idx][i] = '.'; row[idx] = col[i] = diag[idx + i] = rediag[idx - i + n - 1] = false; } } } function check(idx, i) { return !(row[idx] || col[i] || diag[idx + i] || rediag[idx - i + n - 1]); } dfs(0); return ans;};\n搜索插入位置\n35.\n搜索插入位置 - 力扣(LeetCode)\n二分查找的题,记住这个二分经典模板即可。\n二分经典模板即可解决这道题。\n/** * @param {number[]} nums * @param {number} target * @return {number} */var searchInsert = function (nums, target) { let left = 0, right = nums.length - 1; while (left <= right) { const mid = (left + right) >> 1; if (nums[mid] === target) { return mid; } if (nums[mid] > target) { right = mid - 1; } else { left = mid + 1; } } return left;};\n搜索二维矩阵\n74.\n搜索二维矩阵 - 力扣(LeetCode)\n二维矩阵的搜索,从二分角度考虑,要寻找中间值,从题目中的排序规律可以看到,右上角的值处于中间位置,向左减小,向下增大。\n/** * @param {number[][]} matrix * @param {number} target * @return {boolean} */var searchMatrix = function (matrix, target) { let x = 0, y = matrix[0].length - 1; while (x < matrix.length && y >= 0) { if (matrix[x][y] === target) { return true; } else if (matrix[x][y] > target) { y--; } else { x++; } } return false;};\n排序数组中查找第一个和最后一个位置\n34.\n在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)\n模板返回大于等于某个值的最小下标、小于等于某个值的最大下标。\n结果需要注意特殊情况,如果大于等于的时候,当返回的结果是nums.length时,表示数组为空或者所有的值都小于target。如果小于等于的时候,返回的结果是-1时,表示数组为空或者所有的值都大于target。\n当大于等于的时候最终的left为结果,小于等于的时候最终的right为结果。\n只记住大于等于的模板即可,反过来的可以类比一下推出来。\n/** * @param {number[]} nums * @param {number} target * @return {number[]} */var searchRange = function (nums, target) { function biSearch(nums, target, type) { let left = 0, right = nums.length - 1; // 注意这里是left <= right while (left <= right) { const mid = (left + right) >> 1; if (type === 0) { if (nums[mid] >= target) { right = mid - 1; } else { left = mid + 1; } } else { if (nums[mid] <= target) { left = mid + 1; } else { right = mid - 1; } } } return type === 0 ? left : right; } const start = biSearch(nums, target, 0); // 注意特殊情况处理。 if (start === nums.length || nums[start] !== target) { return [-1, -1]; } const end = biSearch(nums, target, 1); return [start, end];};\n搜索旋转排序数组\n33.\n搜索旋转排序数组 - 力扣(LeetCode)\n/** * @param {number[]} nums * @param {number} target * @return {number} */var search = function (nums, target) { if (nums.length === 0) { return -1; } let left = 0, right = nums.length - 1; // 注意是left <= right while (left <= right) { const mid = (left + right) >> 1; if (nums[mid] === target) { return mid; } // [left, mid]区间部分为非降序的 if (nums[left] <= nums[mid]) { // 这里判断target在[left, mid)这个区间中,右开因为上一步判断了不相等 // 左开因为target和nums[left]可能相等, if (nums[left] <= target && target < nums[mid]) { right = mid - 1; } else { left = mid + 1; } // [mid, right]区间部分为非降序的 } else { if (nums[mid] < target && target <= nums[right]) { left = mid + 1; } else { right = mid - 1; } } } return -1;};\n寻找旋转排序数组中的最小值\n153.\n寻找旋转排序数组中的最小值 - 力扣(LeetCode)\n题目中给定的数组和上一题一样,依然是使用二分,这里和最后一个值进行比较,因为旋转后的最后一个值,是第二段区间的最大值,第一段区间的最小值。\n注意返回的是**left**,可以按照“返回没有等号的那个被赋值的变量”,如**nums[mid] > nums[nums.length - 1]**条件中没有**=**,则返回**left**。\n/** * @param {number[]} nums * @return {number} */var findMin = function (nums) { let left = 0, right = nums.length - 1; while (left <= right) { const mid = (left + right) >> 1; if (nums[mid] > nums[nums.length - 1]) { left = mid + 1; } else { right = mid - 1; } } return nums[left];};\n寻找两个正序数组的中位数\n4.\n寻找两个正序数组的中位数 - 力扣(LeetCode)\n要求O(log(m + 1))的复杂度,可以将问题转化为,两个正序数组中,寻找第k小的数,最多寻找两次即可寻找到两个中位数的位置。\n具体可参考题解:4.\n寻找两个正序数组的中位数 - 力扣(LeetCode)\n/** * @param {number[]} nums1 * @param {number[]} nums2 * @return {number} */var findMedianSortedArrays = function (nums1, nums2) { const sumLen = nums1.length + nums2.length; function findKthSmallest(nums1, start1, end1, nums2, start2, end2, k) { const len1 = end1 - start1 + 1, len2 = end2 - start2 + 1; if (len1 > len2) return findKthSmallest(nums2, start2, end2, nums1, start1, end1, k); if (len1 === 0) return nums2[start2 + k - 1]; if (k === 1) return Math.min(nums1[start1], nums2[start2]); const i = start1 + Math.min(Math.floor(k / 2), len1) - 1; const j = start2 + Math.min(Math.floor(k / 2), len2) - 1; if (nums1[i] < nums2[j]) { return findKthSmallest( nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1) ); } else { return findKthSmallest( nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1) ); } } if (sumLen % 2 === 0) { return ( (findKthSmallest( nums1, 0, nums1.length - 1, nums2, 0, nums2.length - 1, Math.ceil(sumLen / 2) ) + findKthSmallest( nums1, 0, nums1.length - 1, nums2, 0, nums2.length - 1, Math.ceil(sumLen / 2) + 1 )) / 2 ); } else { return findKthSmallest( nums1, 0, nums1.length - 1, nums2, 0, nums2.length - 1, Math.ceil(sumLen / 2) ); }};\n最小栈\n155.\n最小栈 - 力扣(LeetCode)\n方法一:使用辅助栈\n每次入栈时,使用另一个栈保存当前栈里的最小值,同步出入栈,会有额外的空间。\n🍕 方法二:不适用额外空间,栈里保存的是每次入栈的值和最小值的差。\nvar MinStack = function () { this.stk = []; this.minValue = -Infinity;};/** * @param {number} val * @return {void} */MinStack.prototype.push = function (val) { // 初始没有数据,直接插入 if (!this.stk.length) { this.stk.push(0); this.minValue = val; } // 有数据后,首先取差值diff,然后栈里插入diff // 如果diff为正,则说明插入值大,最小值不变,否则说明有更小的,更新最小值 else { const diff = val - this.minValue; this.stk.push(diff); this.minValue = diff > 0 ? this.minValue : val; }};/** * @return {void} */MinStack.prototype.pop = function () { if (this.stk.length) { const diff = this.stk.pop(); // 如果差值大于0,说明插入这一个数时,没有更新最小值,由于diff = val - minValue // 所以反推出val = diff + minValue if (diff > 0) { const top = diff + this.minValue; } // 如果不大于0,说明插入时更新了最小值,最小值即为真实值,所以直接取最小值 // 因为最小值更新了,所以更新回来,diff = val - minValue反推 else { const top = this.minValue; this.minValue = top - diff; } }};/** * @return {number} */MinStack.prototype.top = function () { // 同理 if (this.stk.length) { const diff = this.stk.at(-1); return diff > 0 ? diff + this.minValue : this.minValue; }};/** * @return {number} */MinStack.prototype.getMin = function () { return this.minValue;};/** * Your MinStack object will be instantiated and called as such: * var obj = new MinStack() * obj.push(val) * obj.pop() * var param_3 = obj.top() * var param_4 = obj.getMin() */\n字符串解码\n394.\n字符串解码 - 力扣(LeetCode)\n方法 1\n根据规则,可以看作是一个递归的过程,递归调用。\n用栈是为了匹配括号,首先匹配到最外层的括号,然后对括号内的字符串进行递归调用解码。\n需要注意:数字和括号的前后都可能存在不需要解码的字符串,仍然需要拼接到结果中。\n/** * @param {string} s * @return {string} */var decodeString = function (s) { let ans = ''; const stk = []; // 记录需要解码的字符串的其实位置,主要用于拼接前半部分不用解码的字符串 let start = 0; for (let i = 0; i < s.length; i++) { if (s[i] === '[') { stk.push(i); } else if (s[i] === ']') { if (stk.length === 1) { const idx = stk.pop(); let j = idx - 1; while (j >= 0 && '0' <= s[j] && s[j] <= '9') j--; j++; const cnt = Number(s.slice(j, idx)); ans = ans .concat(s.slice(start, j)) .concat(decodeString(s.slice(idx + 1, i)).repeat(cnt)); start = i + 1; } else { stk.pop(); } } } // 如果没有到最后s.length,说明最后的字符串为不用解码的 if (start !== s.length) { ans = ans.concat(s.slice(start)); } return ans || s;};\n方法 2\n按顺序依次遍历,栈中存储要循环的次数以及当前循环之前的结果。\nvar decodeString = function (s) { const arr = [...s]; let ans = []; const stk = []; let cnt = 0; for (let i = 0; i < arr.length; i++) { if (arr[i] <= '9' && arr[i] >= '0') { cnt = cnt * 10 + Number(arr[i]); } else if (arr[i] === '[') { stk.push([cnt, ans.slice()]); cnt = 0; ans = []; } else if (arr[i] === ']') { const [multi, ret] = stk.pop(); ret.push(...[...ans.join('').repeat(multi)]); ans = ret; } else { ans.push(arr[i]); } } return ans.join('');};\n每日温度\n739.\n每日温度 - 力扣(LeetCode)\n使用单调栈,栈底到栈顶元素依次递减。只要元素在栈中,就说明目前没有发现后续有比该元素大的。\n栈里存储的是元素下标,方便计算差了多少天。\n如果栈空,直接入栈\n否则,如果元素比栈顶元素大,说明栈顶元素后的第一个大的就是当前元素,取出来,计算差值。\n否则,跳过直接加入到栈中。\n/** * @param {number[]} temperatures * @return {number[]} */var dailyTemperatures = function (temperatures) { const stk = [], ans = new Array(temperatures.length).fill(0); for (let i = 0; i < temperatures.length; i++) { while (stk.length && temperatures[stk.at(-1)] < temperatures[i]) { const t = stk.pop(); ans[t] = i - t; } stk.push(i); } return ans;};\n数组中第 K 个最大的元素\n215.\n数组中的第 K 个最大元素 - 力扣(LeetCode)\n要求用O(n)的复杂度,数学证明快速选择算法为O(n)复杂度,因此直接使用即可\n/** * @param {number[]} nums * @param {number} k * @return {number} */var findKthLargest = function (nums, k) { function quickChoice(nums, left, right, k) { if (left >= right) return nums[left]; let less = left - 1, i = left, more = right + 1; const x = nums[Math.floor((left + right) / 2)]; while (i < more) { if (nums[i] < x) { less++; [nums[i], nums[less]] = [nums[less], nums[i]]; i++; } else if (nums[i] > x) { more--; [nums[i], nums[more]] = [nums[more], nums[i]]; } else { i++; } } if (less < k && k < more) return nums[k]; if (k <= less) return quickChoice(nums, left, less, k); if (k >= more) return quickChoice(nums, more, right, k); } return quickChoice(nums, 0, nums.length - 1, nums.length - k);};\n前 K 个高频元素\n347.\n前 K 个高频元素 - 力扣(LeetCode)\n题目要求复杂度优于O(nlogn),前 K\n个元素很容易联想到堆这种数据结构。\n要求频率大小,首先可以遍历数组求出每个元素出现的次数,然后用大小为K\n的小根堆存储次数,使用小根堆原因是:如果新元素大于堆顶,说明在已知的前\nK 个元素中,新元素能把堆顶元素踢掉(堆顶肯定不是前 K\n个了),剩余的元素都大于堆顶。直到遍历完所有的频率次数组合。\n/** * @param {number[]} nums * @param {number} k * @return {number[]} */var topKFrequent = function (nums, k) { const numCnt = new Map(); for (const num of nums) { if (numCnt.has(num)) { numCnt.set(num, numCnt.get(num) + 1); } else { numCnt.set(num, 1); } } const heap = []; // 比较函数,返回负数时,parent在son前边,为0时,顺序不变,正数时parent在son后边 const fn = (parent, son) => { return parent.cnt - son.cnt; }; for (const [num, cnt] of numCnt) { if (heap.length < k) { heap.push({ num, cnt }); heapUp(heap, heap.length - 1, fn); } else if (heap[0].cnt < cnt) { heap[0] = { num, cnt }; heapDown(heap, 0, k, fn); } } return heap.map((item) => item.num);};function heapUp(arr, idx, fn) { let parentIdx; // 比较函数结果为正数时才交换 while (fn(arr[(parentIdx = Math.trunc((idx - 1) / 2))], arr[idx]) > 0) { swap(arr, parentIdx, idx); idx = parentIdx; }}function heapDown(arr, idx, size, fn) { let t = idx, left = 2 * idx + 1, right = 2 * idx + 2; if (left < size && fn(arr[t], arr[left]) > 0) { t = left; } if (right < size && fn(arr[t], arr[right]) > 0) { t = right; } if (t !== idx) { swap(arr, t, idx); heapDown(arr, t, size, fn); }}function swap(arr, i, j) { const t = arr[i]; arr[i] = arr[j]; arr[j] = t;}\n跳跃游戏\n55.\n跳跃游戏 - 力扣(LeetCode)\n贪心算法,如果能到达i,那么从i位置能到达的最远的位置是i + nums[i],记为maxDistance,然后继续向后计算,如果i + 1 <= maxDistance,就说明在上一步的前提下,可以到达i + 1位置,然后基于该位置更新maxDistance。初始时最远位置是\n0。\nclass Solution {public: bool canJump(vector<int>& nums) { int maxDistance = 0; for (int i = 0; i < nums.size(); i++) { if (i <= maxDistance) { maxDistance = max(maxDistance, i + nums[i]); } } return maxDistance >= nums.size() - 1; }};\n跳跃游戏 Ⅱ\n45.\n跳跃游戏 II - 力扣(LeetCode)\n贪心算法,题目中保证一定可以到最终位置。在位置i时,可以到达的最远位置为i + nums[i],那么是不是应该跳到i + nums[i]位置呢?不一定。从i位置最远可以跳到i + nums[i],如果j位置(i <= j <= i + nums[i]),并且从j位置跳的最远距离在这个区间中时最远的,那么下一步应该跳到j位置,保证下一步跳的最远。\nclass Solution {public: int jump(vector<int>& nums) { int maxPos = 0, ans = 0, i = 0; while (maxPos < nums.size() - 1) { int end = 0; for (int j = i; j <= maxPos; j++) { end = max(end, j + nums[j]); } i = maxPos; maxPos = end; ans++; } return ans; }};\n完全平方数\n279.\n完全平方数 - 力扣(LeetCode)\n动态规划,f[i]表示i这个数的完全平方数的最少数量。\n由定义可知,组成i这个数的完全平方数的范围肯定在之间,因此可以遍历一遍,设j在这个区间内,那么如果完全平方数包含j这个数,剩余的数的最少数量则由f[i - j * j]表示,可以看成子问题,使用动态规划做。\n/** * @param {number} n * @return {number} */var numSquares = function (n) { const f = new Array(n + 1).fill(Infinity); f[0] = 0; for (let i = 1; i < f.length; i++) { for (let j = 1; j <= i / j; j++) { f[i] = Math.min(f[i], f[i - j * j] + 1); } } return f.at(-1);};\n零钱兑换\n322.\n零钱兑换 - 力扣(LeetCode)\n思路同上\n/** * @param {number[]} coins * @param {number} amount * @return {number} */var coinChange = function (coins, amount) { const f = new Array(amount + 1).fill(Infinity); f[0] = 0; for (let i = 1; i < f.length; i++) { for (const coin of coins) { if (i >= coin) { f[i] = Math.min(f[i], f[i - coin] + 1); } } } return f.at(-1) === Infinity ? -1 : f.at(-1);};\n单词拆分\n139.\n单词拆分 - 力扣(LeetCode)\n动态规划,f[i]表示以前i个字符串能否被表示出来,遍历单词字典,如果前i个字符串的最后的word.size()长度的子字符串和word相等,则说明可以通过word组成,再判断f[i - word.size()]能否组成即可,子问题。\n/** * @param {string} s * @param {string[]} wordDict * @return {boolean} */var wordBreak = function (s, wordDict) { const f = new Array(s.length + 1).fill(false); f[0] = true; for (let i = 1; i <= s.length; i++) { for (const word of wordDict) { if (word.length > i) continue; if (s.slice(i - word.length, i) === word) { f[i] = f[i - word.length]; } if (f[i]) break; } } return f.at(-1);};\n最长递增子序列\n300.\n最长递增子序列 - 力扣(LeetCode)\n要求O(nlogn)的复杂度,考虑二分查找。\n状态定义:f[i]表示长度为i的递增子序列的最后一个元素所有可能的取值的最小值。\n根据定义,猜想:f数组是递增的。\n反证法:如果f[k] >= f[i]且k = i - 1,那么在以f[i]结尾的子序列中,倒数第二个元素(记为v)一定小于f[i],以v为结尾的子序列长度为k。因为f[i] > v且f[k] >= f[i]所以f[k] > v,与f的定义矛盾。因为长度为k的子序列的最后一个元素是所有可能取值里最小的,初始条件取的f[k]但是推出的结论v < f[k],说明f[k]不是最小,矛盾。\n每遍历一个数,从f中求小于num的最大的位置(也就是大于等于num的最小的位置)\nclass Solution {public: int lengthOfLIS(vector<int>& nums) { // f[i] 表示递增子序列长度为 i 时的子序列的最后一个元素的可能的最小值 vector<int> f(nums.size() + 1, 0); int len = 0; for (int i = 0; i < nums.size(); i++) { int left = 1, right = len; while (left <= right) { int mid = left + right >> 1; // 注意这里二分的含义是 大于等于 nums[i] 的最小的,不能理解为 小于 nums[i] 的最大的 // 如果找得到则 left 正好是对应的位置,如果找不到,即所有的数都小于 nums[i] ,则结果为 右边界 + 1 if (f[mid] >= nums[i]) right = mid - 1; else left = mid + 1; } len = max(len, left); f[left] = nums[i]; } return len; }};\n乘积最大子数组\n152.\n乘积最大子数组 - 力扣(LeetCode)\n动态规划,f[i]表示以nums[i]为结尾的子数组的最大乘积,但是这样会出现问题,因为每个元素可能会出现负值,如果是负值,那么期望前一个子数组的乘积越小越好,这与最初定义相反。因此可以维护另一个数组,存储子数组的最小乘积。\nmaxF[i]表示子数组最大乘积,minF[i]表示子数组最小乘积。\n每次遍历时最大值从maxF[i - 1] * nums[i], minF[i - 1] * nums[i], nums[i]中取,最小值同理。\nclass Solution {public: int maxProduct(vector<int>& nums) { vector<int> maxF(nums.size(), 0); vector<int> minF(nums.size(), 0); maxF[0] = minF[0] = nums[0]; for (int i = 1; i < nums.size(); i++) { maxF[i] = max(max(maxF[i - 1] * nums[i], minF[i - 1] * nums[i]), nums[i]); minF[i] = min(min(minF[i - 1] * nums[i], maxF[i - 1] * nums[i]), nums[i]); } return *max_element(maxF.begin(), maxF.end()); }};\n分割等和子集\n416.\n分割等和子集 - 力扣(LeetCode)\n可以改造成背包问题,分成两个子集,两个子集的和相等,转化为找一个子集的和恰好为整个数组的和的一半,即选取一些数,这些数的和恰好等于总和的一半。\n和原背包不同的是原背包是小于体积,这个问题是恰好等于“体积”。\nclass Solution {public: bool canPartition(vector<int>& nums) { int sum = 0; for (int num : nums) sum += num; if (sum % 2 != 0) return false; // f[i][j] 表示拿前 i 个物品(数)的累加和是否恰好等于 j vector<vector<bool>> f(nums.size() + 1, vector<bool>(sum / 2 + 1, false)); for (int i = 0; i < f[0].size(); i++) f[0][i] = false; for (int i = 0; i < f.size(); i++) f[i][0] = true; for (int j = 1; j < f[0].size(); j++) { for (int i = 1; i < f.size(); i++) { // 如果当前要取的数小于或等于“体积”,可以选择拿或者不拿 // 如果不拿,则是否恰好等于 j 取决于前 i - 1 个数的条件 // 如果拿,则取决于前 i - 1 个数体积为 j - nums[i - 1] 的条件,因为要保证拿了这个数恰好等于 j,所以要减去 nums[i - 1] if (nums[i - 1] <= j) f[i][j] = f[i - 1][j] || f[i - 1][j - nums[i - 1]]; // 如果大于,则肯定拿不了 else f[i][j] = f[i - 1][j]; } } return f.back().back(); }};\n最长回文子串\n5.\n最长回文子串 - 力扣(LeetCode)\n如果字符串长度为 1,则肯定是回文串,直接返回,如果字符串长度为\n2,则判断两个字符是否相等,如果相等,则返回,否则最长回文子串就是一个字符。\n其余情况:\n使用f[i][j]表示子串s[i]...s[j]是否是回文子串,是不是回文子串取决于s[i] s[j]是不是相等,如果不相等,则肯定不是回文的,否则判断f[i + 1][j - 1]是不是回文的。\n如果f[i + 1][j - 1]是回文的,则f[i][j]是不是回文取决于在前后追加的字符是否相同。\nclass Solution {public: string longestPalindrome(string s) { const int n = s.size(); if (n == 1) return s; if (n == 2) return s[0] == s[1] ? s : string(1, s[0]); vector<vector<bool>> f(n, vector<bool>(n, false)); // 初始化长度为 1 和为 2 的情况 for (int i = 0; i < n; i++) { f[i][i] = true; if (i > 0) f[i - 1][i] = s[i - 1] == s[i]; } // 从长度为 3 开始遍历 for (int len = 3; len <= n; len++) { for (int i = 0; i + len - 1 < n; i++) { int j = i + len - 1; f[i][j] = f[i + 1][j - 1] && s[i] == s[j]; } } int maxLen = 1; int x = 0, y = 0; for (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { if (f[i][j] && maxLen < j - i + 1) { maxLen = j - i + 1; x = i, y = j; } } } return s.substr(x, maxLen); }};\n下一个排列\n31.\n下一个排列 - 力扣(LeetCode)\n按照字典序求下一个排列,题解:31.\n下一个排列 - 力扣(LeetCode)\n流程:\n\n从后往前找第一个相邻的升序对 (i, j)\n在 [j, end)\n这个区间中从后向前寻找第一个比\nnums[i]大的数nums[k]\n交换 nums[i] nums[k]\n再将 [j, end) 的区间逆序。\n\n总体思路:\n\n将后边较大的数与前边较小的数交换,即可以让数值更大。\n因为是下一个排列,同时也希望增大的不是特别快。所以希望较大的数尽量的小。\n交换完后,后续的数肯定是降序,反转改成升序。\n\nclass Solution {public: void nextPermutation(vector<int>& nums) { if (nums.size() <= 1) return; int i = nums.size() - 2, j = i + 1, k = j; for (; i >= 0; i--, j--) { if (nums[i] < nums[j]) break; } // 如果 i >= 0 说明不是最后一个排列,可以继续向下寻找,否则说明是最后一个排列,则直接跳过 // 执行后续步骤就会回到第一个排列 if (i >= 0) { for (; k >= j; k--) { if (nums[i] < nums[k]) break; } swap(nums[i], nums[k]); } for (int a = j, b = nums.size() - 1; a < b; a++, b--) { swap(nums[a], nums[b]); } }};\n寻找重复数\n287.\n寻找重复数 - 力扣(LeetCode)\n看成链表的形式,如果存在重复的数,说明链表中存在环。链表有环判断,通过快慢指针。142. 环形链表\nII - 力扣(LeetCode)\nclass Solution {public: int findDuplicate(vector<int>& nums) { int fast = 0, slow = 0; while (true) { fast = nums[nums[fast]]; slow = nums[slow]; if (fast == slow) { slow = 0; while (fast != slow) { fast = nums[fast]; slow = nums[slow]; } return slow; } } return 0; }};\n数据流中的中位数\n295.\n数据流的中位数 - 力扣(LeetCode)\n要求低时间复杂度,根据中位数的定义,使用两个优先队列(堆)存储,一个保存中位数前半部分,一个保存后半部分,加入元素时,判断属于哪一部分,加入完毕后,判断两个队列长度是否相等或者后半部分长度多一个。否则将多的数加入到另一个队列。\nclass MedianFinder {public: priority_queue<int, vector<int>, less<int>> largeHeap; priority_queue<int, vector<int>, greater<int>> smallHeap; MedianFinder() { } void addNum(int num) { if (smallHeap.empty() || num >= smallHeap.top()) { smallHeap.push(num); if (smallHeap.size() > largeHeap.size() + 1) { largeHeap.push(smallHeap.top()); smallHeap.pop(); } } else { largeHeap.push(num); if (largeHeap.size() > smallHeap.size()) { smallHeap.push(largeHeap.top()); largeHeap.pop(); } } } double findMedian() { if (smallHeap.size() == largeHeap.size()) return 1.0 * (smallHeap.top() + largeHeap.top()) / 2; return smallHeap.top(); }};\n柱状图中最大的矩形\n84.\n柱状图中最大的矩形 - 力扣(LeetCode)\nclass Solution {public: int largestRectangleArea(vector<int>& heights) { int n = heights.size(); // 单调递增栈 stack<int> st; // 分别保存第 i 个元素的左边、右边的最近的比 heights[i] 小的位置 vector<int> left(n), right(n); for (int i = 0; i < n; i++) { while (!st.empty() && heights[i] <= heights[st.top()]) { st.pop(); } // 如果栈空,说明左边的元素都比当前元素大,在之前的存在栈中的元素中当前元素最小 // 直接设为 -1 (哨兵值,方便后期计算) left[i] = st.empty() ? -1 : st.top(); st.push(i); } stack<int>().swap(st); for (int i = n - 1; i >= 0; i--) { while (!st.empty() && heights[i] <= heights[st.top()]) { st.pop(); } right[i] = st.empty() ? n : st.top(); st.push(i); } int ans = 0; for (int i = 0; i < n; i++) { // right[i] 和 left[i] 存储的是右边和左边的最近的最小的位置,按照 // height[i] 高度寻找最大的矩形,left[i] 和 right[i] 位置的高度都无法参与构成 // 因此要剔除这两个,所以需要多减一个 1 ans = max(ans, (right[i] - left[i] - 1) * heights[i]); } return ans; }};\n最长有效括号\n32.\n最长有效括号 - 力扣(LeetCode)\nf[i]表示以i位置为结尾的最长有效括号长度。\n如果s[i] == '('则肯定不是有效括号\n如果s[i] == ')',则如果s[i - 1] == '(',最后两个元素可以构成有效括号,再加上f[i - 2]的长度即可。则如果s[i - 1] == ')',判断s[i - f[i - 1] - 1] == '('(判断以s[i - 1]结尾的有效长度的上一个位置是否是左括号),相等的话,则说明[i - f[i - 1] - 1], i]的范围内,两边可以组成括号,中间内部能否组成由f[i - 1]决定,最后加上f[i - f[i - 1] - 2]的长度。\nclass Solution {public: int longestValidParentheses(string s) { if (s.size() == 0) return 0; vector<int> f(s.size(), 0); for (int i = 0; i < s.size(); i++) { if (s[i] == '(') continue; if (i > 0 && s[i - 1] == '(') { f[i] = i > 1 ? f[i - 2] + 2 : 2; } else if (i > 0 && s[i - 1] == ')') { if (i - f[i - 1] - 1 >= 0 && s[i - f[i - 1] - 1] == '(') { f[i] = f[i - 1] + (i - f[i - 1] - 2 >= 0 ? f[i - f[i - 1] - 2] : 0) + 2; } } } return *max_element(f.begin(), f.end()); }};\n","categories":["算法"],"tags":["LeetCode"]},{"title":"docker配置学习","url":"/2023/10/23/docker%E9%85%8D%E7%BD%AE%E5%AD%A6%E4%B9%A0/docker%E9%85%8D%E7%BD%AE%E5%AD%A6%E4%B9%A0/","content":"安装docker\n\n更新软件库确保可以访问最新版。\n\nsudo apt update\n\n卸载旧版本\n\nsudo apt-get remove docker docker-engine docker.io\n\n\n安装软件库中的docker\n\nsudo apt install docker.io\n\n设置自启动\n\nsudo systemctl start dockersudo systemctl enable docker\n\n其他安装方法的可以参考官方文档Install Docker\nEngine on Ubuntu | Docker Docs\n\n拉取Redis镜像并运行\n以拉取redis镜像为例。\n\n拉取\n\n使用命令sudo docker pull redis:6.2拉取tag为6.2的redis镜像。\n不指定tag则默认为latest,若要查看所有tag可以使用命令sudo docker search redis查看。或者在Docker Hub Container Image Library | App\nContainerization中查找。\n\n准备redis.conf\n\n在redis官网Redis\nconfiguration |\nRedis找到对应版本的默认配置。创建redis.conf文件。\n并修改\n将bind 127.0.0.1 注释将protected-mode yes修改为 protected-mode no\n\n运行\n\nsudo docker run -d -v /home/username/software/redis:/usr/local/etc/redis -p 36379:6379 --name myredis redis:6.2 redis-server /usr/local/etc/redis/redis.conf\n其中/home/username/software/redis目录下有redis.conf文件。\n这段命令中-d是后台运行容器。\n-v /home/username/software/redis:/usr/local/etc/redis是将/home/username/software/redis目录挂载到/usr/local/etc/redis目录下,因为redis容器运行时不会生成redis.conf配置文件,以默认配置运行,需要运行时手动挂在自定义配置。\n-p 36379:6379是端口映射,将宿主机36379段口映射到容器的6379端口。\n--name myredis指定容器名字为myredis。\nredis:6.2为运行的镜像名字:tag。\nredis-server /usr/local/etc/redis/redis.conf为运行容器后执行的命令,此命令是让redis服务器以/usr/local/etc/redis/redis.conf配置文件运行。\n拉取mysql并运行\n拉取mysql:8.2镜像\ndocker pull mysql:8.2\n运行镜像,同redis一样,需要指定挂载目录,将自己的配置文件放在本地目录,挂载到容器中相应的目录中。\n其中挂载到哪个目录可以通过官方文档查看。\ndocker run -d \\--name mymysql1 \\-p 33306:3306 \\-v /home/clab/software/mysql/conf:/etc/my.cnf.d \\-v /home/clab/software/mysql/data:/var/lib/mysql \\-v /home/clab/software/mysql/log:/var/log/mysql \\-e MYSQL_ROOT_PASSWORD=password \\mysql:8.2\n如果启动后就退出,使用命令docker logs mymysql1查看日志。\n出现\nERROR] Could not open file '/var/log/mysql/error.log' for error logging: Permission denied\n这种权限报错,原因是启动容器中的mysql用户和宿主机不同,导致权限问题。此时可以使用命令cat /etc/group查看宿主机的user_id\n获取后在执行命令时添加参数--user user_id即可。如果仍出现问题,将宿主机data和log文件夹删除再新建(可能之前的运行会导致这两个文件夹用户权限发生变化)。\n参考链接:分析docker启动MySQL挂载目录提示权限不足Permission\ndenied原因_docker挂载目录权限问题_jiangyunfan16的博客-CSDN博客\n"},{"title":"grpc学习","url":"/2023/10/01/grpc/gRPC%E5%AD%A6%E4%B9%A0/","content":"\n介绍\ngRPC 是一个高性能、通用的开源 RPC 框架,其由 Google\n主要面向移动应用开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf(Protocol\nBuffers) 序列化协议开发,且支持众多开发语言。\n安装\ngRPC 基于 ProtoBuf 序列化协议,因此需要安装 protoc 编译器用于编译\nprotocolbuf(.proto 文件)和 protobuf 运行时。\n下载 protoc\n进入protobuf\n仓库下载预编译好的 protoc 二进制文件。 我这里是 windows\n环境,因此选择 win 的版本下载。\n下载完成后,解压到自己的相关目录即可。然后配置环境变量。\n然后执行命令protoc --version,输出对应版本表示安装成功。\n下载 go plugins\n安装好 protoc 编译器后,还需要安装相关语言的插件,比如这里使用 go\n语言,需要安装 go 语言的插件,插件会将编译后的结果生成为 go\n代码供我们使用。 可以直接使用 go 命令安装。\ngo install google.golang.org/protobuf/cmd/protoc-gen-go@latestgo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest\n相关的执行文件会安装到$GOPATH/bin目录下。 还需要其他\ngrpc 相关的包,这里不单独安装,在运行代码时再进行安装。\nDEMO\ngRPC 主要有 4 种请求和响应模式,分别是简单模式(Simple\nRPC)、服务端流式(Server-side streaming RPC)、客户端流式(Client-side\nstreaming RPC)、和双向流式(Bidirectional streaming RPC)。\n\n简单模式(Simple RPC):客户端发起请求并等待服务端响应。\n服务端流式(Server-side streaming\nRPC):客户端发送请求到服务器,拿到一个流去读取返回的消息序列。\n客户端读取返回的流,直到里面没有任何消息。\n客户端流式(Client-side streaming\nRPC):与服务端数据流模式相反,这次是客户端源源不断的向服务端发送数据流,而在发送结束后,由服务端返回一个响应。\n双向流式(Bidirectional streaming\nRPC):双方使用读写流去发送一个消息序列,两个流独立操作,双方可以同时发送和同时接收。\n\n简单模式\n项目根目录为LearnGo,该目录下的 gRPC 相关目录结构为:\n\n定义服务\n通过 protobuf\n语法定义和编程语言、平台无关的接口。文件test.proto内容为:\nsyntax = \"proto3\";// option go_package = \"path;name\";// path 表示生成的go文件的存放地址,如果目录不存在会自动生成目录// name 表示生成的go文件所属的包名option go_package = \"./;proto\";// 定义包名package proto;// 定义消息message HelloRequest { string name = 1;}message HelloReply { string message = 1;}// 定义服务service Greeter { // 定义方法,接受的HelloRequest消息,返回HelloReply消息 rpc SayHello(HelloRequest) returns (HelloReply) {}}\n编译\nprotoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative test.proto\n编译完成后,会生成一个test.pb.go和test_grpc.pb.go文件。\n我使用的Intellij IDEA打开的项目,进入到生成的文件中,会报错提示找不到包,可以直接使用\nIDE 安装相关包即可。 每次编译都需要执行命令可能会很麻烦,可以在 IDEA\n中定义工具菜单,简化操作。 首先打开设置菜单 添加工具,\n 按照自己的指令进行设置即可。\n--go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative $FileName$\n然后选中xxx.proto文件,找到External Tools选择即可。\n服务端代码\npackage mainimport ( // 自己编写的proto文件生成的包\tpb \"LearnGo/test05/proto\"\t\"golang.org/x/net/context\"\t\"google.golang.org/grpc\"\t\"google.golang.org/grpc/reflection\"\t\"log\"\t\"net\")// 定义server,用来实现proto文件里实现的接口type server struct{ pb.UnimplementedGreeterServer}// 实现定义的SayHello接口// 第一个是上下文参数,默认要填;第二个是定义的HelloRequest消息// 返回值是我们定义的HelloReply消息,error返回值也是必须的。func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {\tlog.Printf(\"receive the message: %v\", in.Name)\treturn &pb.HelloReply{Message: in.Name}, nil}func main() { // 设定监听端口\tlis, err := net.Listen(\"tcp\", \"127.0.0.1:50051\")\tif err != nil {\t\tlog.Fatalf(\"failed to listen: %v\", err)\t} // 实例化gRPC服务器\ts := grpc.NewServer() // 注册服务\tpb.RegisterGreeterServer(s, &server{}) // 向gRPC服务端注册反射服务\treflection.Register(s) // 启动服务\tif err := s.Serve(lis); err != nil {\t\tlog.Fatalf(\"failed to serve: %v\", err)\t}}\n客户端代码\npackage mainimport ( // 导入生成的proto包\t\"LearnGo/test05/proto\"\t\"context\"\t\"google.golang.org/grpc\"\t\"log\"\t\"time\")func main() { // 连接grpc服务器\tconn, err := grpc.Dial(\"localhost:50051\", grpc.WithInsecure())\tif err != nil {\t\tlog.Fatalf(\"filed to connect: %v\", err)\t} // 延迟关闭连接\tdefer conn.Close() // 新建Greeter服务 客户端\tc := proto.NewGreeterClient(conn) // 初始化上下文,设置请求超时为1秒\tctx, cancel := context.WithTimeout(context.Background(), time.Second) // 延迟关闭会话\tdefer cancel() // 调用SayHello接口发送消息\treply, err := c.SayHello(ctx, &proto.HelloRequest{Name: \"test grpc\"})\tif err != nil {\t\tlog.Fatalf(\"failed to send: %v\", err)\t} // 打印服务的返回消息\tlog.Printf(\"the reply message: %v\", reply.Message)}\n执行\n分别执行server.go和client.go程序,可以看到打印的结果\n服务端打印: 客户端打印:\n","categories":["技术"],"tags":["grpc"]},{"title":"Hexo配置","url":"/2023/12/01/hexo/hexo%E9%85%8D%E7%BD%AE/","content":"插件安装\nnpm install hexo-asset-img --save\n该插件可以将相对路径转换为绝对路径使得浏览器可以访问到。\n\n配置Hexo\n修改根目录下的_config.yml文件。\n将post_asset_folder设置为true。该配置会在使用hexo new \"filename\"时自动生成同名的文件夹保存资源。\n配置Typora\n将Typora的图像设置改为保存到与该文件同目录的同名文件夹中。\n\nHexo配置渲染数学公式\n\n卸载默认渲染器,安装Pandoc渲染器\nnpm uninstall hexo-renderer-marked --savenpm install hexo-renderer-pandoc --save\n安装mathjax包\nnpm install hexo-filter-mathjax\n在根目录_config.yml下添加\nmathjax: tags: none # or 'ams' or 'all' single_dollars: true # enable single dollar signs as in-line math delimiters cjk_width: 0.9 # relative CJK char width normal_width: 0.6 # relative normal (monospace) width append_css: true # add CSS to every page every_page: true # if true, every page will be rendered by mathjax regardless the `mathjax` setting in Front-matter of each article\n在博客front-matter上添加\nmathjax: true\n\n\n配置显示emoji\n安装库\nnpm install hexo-filter-emoji\n在_config.yml添加以下内容:\n# hexo-filter-emojiemoji: enable: true className: github-emoji styles: customEmojis:\n","categories":["Hexo"],"tags":["Hexo插件"]},{"title":"k8s部署安装","url":"/2023/12/01/k8s/k8s%E5%AE%89%E8%A3%85/","content":"此过程参考博客。\n只记录和博客中有出入的地方。\n\n5.1.2\n\n博客nginx中该配置,参数$remote_addr,如果直接复制到终端中可能会出现配置文件中$remote_addr位置为空,终端会认为$remote_addr是一个变量,而实际上是这是一个字符串输入到文件中。\n9.1 方式一\n配置完成后等待pod执行,如果一直出现imagebackoffpull等问题,可以使用命令kubectl describe pod <pod name> -n kube-system查看具体报错,通常原因是拉取镜像失败。\n可以手动拉取镜像,此时要注意,使用的containerd容器运行时,k8s默认会把镜像拉取到k8s.io命名空间中,因此手动拉取时也需要拉取到这个命名空间中\nctr -n k8s.io image pull <image_name>\n11.1.1\n按照配置博客配置后如果出现metrics server无法访问等错误,可以尝试在配置文件中添加参数\nhostNetwork: true\n.pjrdlpemsxxj{zoom:50%;}\n\n本地调试scheduler\n\n将远程环境中用户目录的.kube目录拷贝到本地windows用户目录下。\n该目录下配置clusters.cluster.server配置是本机18443端口127.0.0.1:18443\n将本地启动端口18443,与远程主机192.168.217.100连接登录然后将端口转发到127.0.0.1:8443,相当于在远程主机本地访问集群ip和端口。\nssh -L 18443:127.0.0.1:8443 -N -f [email protected]\nssh -L localport:remotehost:remotehostport sshserver说明:localport 本机开启的端口号remotehost 最终连接机器的IP地址remotehostport 最终连接机器的端口号sshserver 转发机器的IP地址选项:-f 后台启用-N 不打开远程shell,处于等待状态(不加-N则直接登录进去)-g 启用网关功能\n\n安装kube-prometheus\nhttps://cloud.tencent.com/developer/article/2216613\nhttps://cloud-atlas.readthedocs.io/zh-cn/latest/kubernetes/monitor/prometheus/helm3_prometheus_grafana.html\n网络问题是最主要的问题,可以多尝试几次。\n启动pod时拉取镜像中,kube-state-metrics这个pod的镜像拉取最容易失败,即使手动拉取也非常容易失败。\n可以参考链接采用其他的kube-state-metrics安装,然后将拉取的镜像重新改名为上述需要的镜像。\n\n或者不使用helm可以参考该链接:https://blog.csdn.net/slc09/article/details/132571091\n\n访问相关页面:\nalertmanager-main: 59.65.191.100:31675\ngrafana: 59.65.191.100:32657\nprometheus-k8s: 59.65.191.100:32678\n一直Terminating的解决办法\nhttps://juejin.cn/post/7210020384044335165\nregistry.k8s.io拉取不下来\nk8s官方镜像代理加速_registry.k8s.io-CSDN博客\n","categories":["k8s"],"tags":["k8s","容器"]},{"title":"Java基础","url":"/2023/12/01/%E5%85%AB%E8%82%A1/Java%E5%9F%BA%E7%A1%80/","content":"\n注:大多数内容参考了JavaGuide\nJava\n线程池可以设置的参数\n\nhttps://javabetter.cn/interview/java-34.html#_28-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%9C%89%E5%93%AA%E4%BA%9B%E5%8F%82%E6%95%B0七个参数:1.corePoolSize:核心线程数,线程池中始终存活的线程数。2.maximumPoolSize:\n最大线程数,线程池中允许的最大线程数。3.keepAliveTime:\n存活时间,线程没有任务执行时最多保持多久时间会终止。4.unit:\n单位,参数keepAliveTime的时间单位,7种可选。5.workQueue:\n一个阻塞队列,用来存储等待执行的任务,均为线程安全,7种可选。6.threadFactory:\n线程工厂,主要用来创建线程,默认正常优先级、非守护线程。7.handler:拒绝策略,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。\nsynchronized和可重入锁的区别\n\n用法不同:synchronized\n可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock\n只能用于代码块。\n获取锁和释放锁的机制不同:synchronized\n是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。\n锁类型不同:synchronized 是非公平锁,而\nReentrantLock 默认为非公平锁,也可以手动指定为公平锁。\n响应中断不同:ReentrantLock\n可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。\n底层实现不同:synchronized 是 JVM\n层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。\n\n可重入锁怎么实现\n线程安全的集合有哪些\n\nVector、HashTable\n\n使用synchronized修饰方法保证线程安全效率低\n\nConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet\n\n除了1.8的ConcurrentHashMap大多都是用Lock锁\nConcurrentHashMap怎么保证线程安全\nhttps://javaguide.cn/java/collection/java-collection-questions-02.html#concurrenthashmap-%E5%92%8C-hashtable-%E7%9A%84%E5%8C%BA%E5%88%ABJDK1.7\n的 ConcurrentHashMap 底层采用 分段的数组+链表\n实现,JDK1.8 采用的数据结构跟 HashMap1.8\n的结构一样,数组+链表/红黑二叉树。在 JDK1.7\n的时候,ConcurrentHashMap\n对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。\n到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment\n的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用\nsynchronized 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化)\n整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到\nSegment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。\nCAS是什么\nhttps://javaguide.cn/java/basis/unsafe.html#cas-%E6%93%8D%E4%BD%9C\nCAS 即比较并替换(Compare And\nSwap),是实现并发算法时常用到的一种技术。CAS\n操作包含三个操作数——内存位置、预期原值及新值。执行 CAS\n操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS\n是一条 CPU 的原子指令(cmpxchg\n指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如\ncompareAndSwapObject、compareAndSwapInt、compareAndSwapLong)底层实现即为\nCPU 指令 cmpxchg 。\n存在问题:如果在更新过程中,另一个线程也修改了这个变量,但是修改后的值和原值一样,这样就不会发现修改过,可以采用添加版本号或者时间戳的方式避免。\nvolatile\n\n1.保证内存可见性\n\n当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程读取被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。\n\n2.禁止指令重排序\n\n指令重排序是编译器和处理器为了高效对程序进行优化的手段,cpu\n是与内存交互的,而 cpu 的效率想比内存高很多,所以 cpu\n会在不影响最终结果的情况下,不等待返回结果直接进行后续的指令操作,而\nvolatile\n就是给相应代码加了内存屏障,在屏障内的代码禁止指令重排序。\n\n\nJava的内存区域\nJava的gc逻辑\nhttps://juejin.cn/post/7123853933801373733https://javaguide.cn/java/jvm/jvm-garbage-collection.html\n内存分配和回收原则\n首先分配对象到新生代的Eden区,再次分配对象时仍旧是优先分配到Eden区,如果发现无法满足,则进行一次Minor\ngc(也叫Young\ngc),将Eden区的对象复制到S区,如果S区无法满足条件则根据空间分配担保将对象复制到老年代,一般老年代可以满足条件,如果不满足条件则进行Full\ngc,对新生代和老年代进行一次gc。较大的对象(大对象就是需要大量连续内存空间的对象(比如:字符串、数组))也会直接进入老年代,避免将大对象放入新生代产生频繁的gc操作。具体什么是大对象,不同垃圾回收器会有不同的设置:\n\nG1垃圾回收器可以通过设置参数来确定大对象的大小。\nParallel Scavenge\n垃圾回收器,默认情况没有阈值,是根据当前堆内存的情况和历史数据动态决定的。\n\njvm会给每个对象设置一个年龄,新生代的对象经历一次Minor\ngc存活下来时,会将年龄增加1,当年龄达到一定阈值时(默认为15,但也和具体的垃圾收集器或者参数设置有关),也会加入到老年代中。\n死亡对象判断方法\n\n引用计数法\n\n给对象中添加一个引用计数器:\n\n有一个地方引用了这个对象时,计数器加1\n引用失效则减1\n计数器为0则表示对象不再被使用\n\n该方法实现简单效率高,但是很难解决对象之间循环引用的问题(循环引用的情况下两个对象的计数器都不为0)。\n\n可达性分析算法\n\n通过一系列被称为GC\nRoots的对象作为起点,从这些节点开始向下搜索,节点所走过的路被称为引用链,当一个对象到GC\nRoots没有任何引用链的话,则证明此对象是不可用的,需要被回收。哪些对象可以作为GC\nRoots呢:\n\n虚拟机栈(栈帧中的局部变量表)中引用的对象\n本地方法栈(native方法)中引用的对象\n方法区中类静态属性引用的对象\n方法区中常量引用的对象\n所有被同步锁持有的对象\nJNI(Java Native Interface)引用的对象\n\n\n引用类型总结\n\n强引用:大部分引用都是强引用,垃圾回收器绝不会回收,当内存空间不足时会抛出OOM异常也不会回收强引用的对象。\n软引用:如果内存足够,则不会回收,如果内存不足则会回收软引用对象的内存。软引用可以用来实现内存敏感的高速缓存。\n弱引用:只有短暂的生命周期,无论内存是否充足,只要发现了弱引用的对象就会进行回收。\n虚引用:虚引用并不会决定任何对象的生命周期,对象如果只有虚引用,就和没有引用一样,任何时候都会被回收。\n\n\n虚引用必须和引用队列联合使用,而软引用和弱引用不必须和引用队列联合使用。\n\n如何判断废弃常量\n\n假如在字符串常量池中存在字符串 \"abc\",如果当前没有任何 String\n对象引用该字符串常量的话,就说明常量\"abc\"\n就是废弃常量,如果这时发生内存回收的话而且有必要的话,\"abc\"\n就会被系统清理出常量池了。\n\n如何判断无用类\n\n类需要同时满足下面 3 个条件才能算是 “无用的类”:\n\n该类所有的实例都已经被回收,也就是 Java\n堆中不存在该类的任何实例。\n加载该类的 ClassLoader 已经被回收。\n该类对应的 java.lang.Class\n对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。\n\n虚拟机可以对满足上述 3\n个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。\n垃圾收集算法\n\n标记清除算法\n\n分为标记和清除两个阶段:首先标记出所有不回收的对象,标记完成后统一回收掉所有没有被标记的对象。是最基础的算法,后续算法都是对他的改进,这种算法会有两个明显的问题:\n\n效率问题:标记和清除两个过程效率都不高\n空间问题:标记清楚后会产生大量不连续的内存碎片\n\n\n标记整理算法\n\n标记过程和标记清除算法一样,后续步骤不是直接对可回收对象回收,而是让所有存活的对象向前一端移动,然后直接清理掉端边界以外的内存。由于多了整理的步,效率不高,适合老年代这种垃圾回收频率不是很高的场景。\n\n复制算法\n\n解决效率和内存碎片问题。将内存分为大小相同的两块,每次使用其中一块,当一块使用完后,将存活的对象复制到另一块,然后清理使用的空间。存在两个问题:\n\n可用内存变小\n不适合老年代,如果存活对象数量较多,复制性能会变差。\n\n\n分代收集算法\n\n根据对象存活周期将内存分为几块。一般将Java堆分为新生代和老年代。新生代中每次收集都会有大量对象死去,所以可以选择标记复制算法,只需要少量对象的复制成本就可以完成垃圾收集;而老年代的对象存活几率比较高,而且没有额外的空间担保,所以可以使用标记清除或者标记整理算法。\n垃圾收集器\n垃圾收集器是垃圾收集算法的具体实现。到目前为止还没有最好的垃圾收集器出现,只能根据具体的应用场景选择合适的垃圾收集器。JDK默认的垃圾收集器:\n\n1.8:Parallel Scavenge(新生代) + Parallel Old(老年代)\n9 - 20: G1\n\n\nParallel Scavenge收集器\n\nParallel\nScavenge收集器是使用标记复制算法的多线程收集器,该收集器的关注点在于吞吐量(高效率使用CPU),吞吐量指CPU中用于运行用户代码时间与CPU总消耗时间的比值。该收集器提供了很多参数供用户找到合适的停顿时间或最大吞吐量,也可以使用收集器的自适应调节策略。\n\nParallel Old收集器\n\nParallel\nScavenge的老年代版本,使用多线程的标记整理算法,同样注重吞吐量。\n\nG1收集器\n\nG1 (Garbage-First)\n是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器.\n以极高概率满足GC 停顿时间\n要求的同时,还具备高吞吐量性能特征。有以下特征:\n\n并行与并发:G1 能充分利用\nCPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短\nStop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC\n动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。\n分代收集:虽然 G1\n可以不需要其他收集器配合就能独立管理整个 GC\n堆,但是还是保留了分代的概念。\n空间整合:与 CMS 的“标记-清除”算法不同,G1\n从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。\n可预测的停顿:这是 G1 相对于 CMS\n的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1\n除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为\nM 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。\n\nG1收集器大致分为几个步骤:\n\n初始标记\n并发标记\n最终标记\n筛选回收\n\nG1\n收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的\nRegion(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region\n划分内存空间以及有优先级的区域回收方式,保证了 G1\n收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。\nHashMap的原理\nhttps://javaguide.cn/java/collection/hashmap-source-code.htmljdk1.8之前由\n数组+链表\n组成,链表主要为了解决哈希冲突(拉链法)。在jdk1.8之后解决哈希冲突首先使用链表,当链表长度大于等于阈值(默认为8)时,首先判断数组长度,如果数组长度小于64,会选择扩容,如果数组长度大于64,会将链表转成红黑树,减少搜索的时间。刚刚创建HashMap对象时数组长度为0,当第一次插入数据时数组长度设置为16。首先将对象的hashCode和hashCode无符号右移16位的结果做异或(添加一些扰动)获得实际的哈希值(设为hash),然后将hash和数组长度-1做与运算(等同于取模,因为数组长度为2的n次幂)得到存放的数组索引位置。当键值对的数量size超过阈值(数组长度*负载因子(默认是0.75))就会对数组进行扩容。\n悲观锁和乐观锁的区别、场景\nhttps://javaguide.cn/java/concurrent/optimistic-lock-and-pessimistic-lock.html\n双亲委派机制\n类加载过程\n类加载器详解\n","categories":["八股"],"tags":["Java"]},{"title":"MySQL基础","url":"/2023/12/01/%E5%85%AB%E8%82%A1/MySQL%E5%9F%BA%E7%A1%80/","content":"\n注:大多数内容参考了JavaGuide\nMySQL\nMySQL聚簇索引\n\nhttps://javaguide.cn/database/mysql/mysql-index.html#%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E4%B8%8E%E9%9D%9E%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95索引结构和数据一起存放的索引即聚簇索引,InnoDB中的主键索引属于聚簇索引,B+树的每个非叶子节点存储索引,叶子节点存储索引和对应的数据。优点:\n\n查询速度快,定位到了索引节点就定位到了数据,相比于非聚簇索引少了一次IO操作\n对排序查找和范围查找优化,对于主键的排序查找和范围查找速度快\n\n缺点:\n\n依赖于有序的数据,如果索引属于UUID这种长且难比较的数据,插入或查找速度较慢。\n更新代价大,如果修改了索引列的数据,对应的索引也会被修改,索引对应的叶子节点存放着数据也需要迁移,修改代价较大。所以主键索引,一般不修改主键。\n\nMySQL中为什么使用B+树\n\nHash表\n\n通过key快速查找出对应的value,可以快速检索数据。发生哈希冲突可以采用链地址法解决哈希冲突。但是哈希索引不支持顺序和范围查找。\n\n二叉搜索树BST\n\n二叉搜索树的性能依赖平衡程度,不适合作为MySQL底层索引数据结构。\n\nAVL树\n\n自平衡的二叉搜索树,需要频繁地进行旋转来保持平衡,会有较大的计算开销从而降低数据库写操作的性能;每个AVL树节点只存储一个数据,每次进行磁盘IO只能读取一个节点的数据,如果数据分布在多个节点,则需要进行多次IO。\n\n红黑树\n\n红黑树是一种自平衡二叉查找树,通过在插入和删除节点时进行颜色变换和旋转操作,使得树始终保持平衡状态,它具有以下特点:\n 1. 每个节点非红即黑;\n 2. 根节点总是黑色的;\n 3. 每个叶子节点都是黑色的空节点(NIL 节点);\n 4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);\n 5. 从任意节点到它的叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。\n不追求严格的平衡而是大致的平衡,查询效率稍有下降,因为树可能较高导致需要多次磁盘IO,但是红黑树的插入和删除操作效率大大提高了,红黑树在插入和删除节点时只需进行O(1)次数的旋转和变色操作。\n\nB树和B+树\n\nB 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B\n树的一种变体。B 树和 B+树中的 B 是 Balanced\n(平衡)的意思。目前大部分数据库系统及文件系统都采用 B-Tree\n或其变种 B+Tree 作为索引结构。B 树&\nB+树两者有何异同呢?\n\nB 树的所有节点既存放键(key) 也存放数据(data),而\nB+树只有叶子节点存放 key 和 data,其他内节点只存放 key。\nB\n树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。\nB\n树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而\nB+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。\n在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B\n树进行中序遍历,直到找到查找的上限;而\nB+树的范围查询,只需要对链表进行遍历即可。\n\n综上,B+树与 B 树相比,具备更少的 IO\n次数、更稳定的查询效率和更适于范围查询这些优势。\nB+树索引都存储了什么内容\nB+树叶子节点存放索引和数据,其他节点只存放索引。\nMySQL四种隔离级别\n\n读未提交:最低的隔离界别,允许读取尚未提交的数据变更,可能导致脏读、不可重复读、幻读\n读已提交:允许读取并发事务已经提交的数据,可以阻止脏读,但可能存在不可重复读和幻读\n可重复读:对同一个字段的多次读取结果是一致的,除非事务本身修改数据。可以阻止脏读和不可重复读,幻读仍有可能发生。\n串行化:完全服从ACID的隔离级别,所有事务逐个执行,不会发生干扰。\n\nmvcc机制(多版本并发控制)\nhttps://javaguide.cn/database/mysql/innodb-implementation-of-mvcc.html\nMVCC\n是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC\n会为该事务创建一个数据快照,而不是直接修改实际的数据行。\n\n读操作\n\n当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。具体工作情况如下:\n\n对于读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。\n如果某个数据行有多个版本,事务会选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。\n事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。\n\n\n写操作\n\n当一个事务执行写操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。具体工作情况如下:\n\n对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。\n新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。\n原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。\n\n\n事务提交和回滚\n\n\n当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。\n当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。\n\n\n版本的回收\n\n\n为了防止数据库中的版本无限增长,MVCC\n会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。\n\nMVCC\n通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。\n事务的特性ACID\n\n原子性(Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;\n一致性(Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;\n隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;\n持久性(Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。\n\n","categories":["八股"],"tags":["MySQL"]},{"title":"Spring基础","url":"/2023/12/01/%E5%85%AB%E8%82%A1/Spring%E5%9F%BA%E7%A1%80/","content":"\n注:大多数内容参考了JavaGuide\nSpring\nspring ioc的实现原理\n\nhttps://javabetter.cn/springboot/ioc.html#%E6%98%AF%E4%BD%95https://pdai.tech/md/spring/spring-x-framework-ioc-source-1.html#spring%E8%BF%9B%E9%98%B6--spring-ioc%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3%E4%B9%8Bioc%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1IOC(控制反转)是一种设计思想,而不是一个具体的技术实现。将原本在应用程序中手动创建对象的控制权,交由Spring来管理。将对象之间的相互依赖关系交给\nIoC 容器来管理,并由 IoC\n容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。\nIoC\n容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个\nService 类可能依赖了很多其他的类,假如我们需要实例化这个\nService,你可能要每次都要搞清这个 Service\n所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC\n的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。在\nSpring 中, IoC 容器是 Spring 用来实现 IoC 的载体,\nIoC 容器实际上就是个 Map(key,value),Map\n中存放的是各种对象。Spring 时代我们一般通过 XML 文件来配置\nBean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot\n注解配置就慢慢开始流行起来。\nbean的生命周期\n如果该bean是代理对象,则在执行InstantiationAwareBeanPostProcesor生成代理对象后会发生短路,不会按照正常的生命周期进行。\n\n创建 Bean 的实例:Bean 容器首先会找到配置文件中的\nBean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。\nBean 属性赋值/填充:为 Bean\n设置相关属性和依赖,例如@Autowired 等注解注入的对象、@Value\n注入的值、setter方法或构造函数注入依赖和值、@Resource注入的各种资源。\nBean 初始化:\n\n\n如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入\nBean 的名字。\n如果 Bean 实现了 BeanClassLoaderAware 接口,调用\nsetBeanClassLoader()方法,传入 ClassLoader对象的实例。\n如果 Bean 实现了 BeanFactoryAware 接口,调用\nsetBeanFactory()方法,传入 BeanFactory对象的实例。\n与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。\n如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor\n对象,执行postProcessBeforeInitialization() 方法\n如果 Bean\n实现了InitializingBean接口,执行afterPropertiesSet()方法。\n如果 Bean 在配置文件中的定义包含 init-method\n属性,执行指定的方法。\n如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor\n对象,执行postProcessAfterInitialization() 方法。\n\n\n销毁 Bean:销毁并不是说要立马把 Bean\n给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean\n或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。\n\n\n如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。\n如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的\nBean 销毁方法。或者,也可以直接通过@PreDestroy 注解标记 Bean\n销毁之前执行的方法。\n\nspring aop的\nhttps://javaguide.cn/system-design/framework/spring/spring-knowledge-and-questions-summary.html#spring-aop\n理解\nAOP面向切面编程能够将那些与业务无关,却为业务模块所共同使用的逻辑(事务处理、日志管理、权限控制、限流等)封装起来,便于减少重复代码,降低模块间的耦合度,利于维护。Spring中的AOP基于动态代理,分为JDK的动态代理和CGLib的动态代理。这两个的区别是:\n\nJDK的动态代理要求被代理的对象实现了某个接口,然后会生成一个同样实现了这个接口的代理对象。\n如果没有实现某个接口,CGLib会生成一个被代理对象的子类作为代理对象。\n\nspring\naop是运行时增强,aspectj属于编译时增强。spring\naop基于代理,而aspectj基于字节码操作。\naspectj定义的通知类型有哪些\n\nBefore(前置通知):目标对象的方法调用之前触发\nAfter (后置通知):目标对象的方法调用之后触发\nAfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发\nAfterThrowing(异常通知):目标对象的方法运行中抛出\n/ 触发异常后触发。AfterReturning 和AfterThrowing\n两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值\nAround\n(环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后操作,甚至不调用目标对象的方法\n\n多个切面的执行顺序如何控制\n\n使用@Order注解\n实现Ordered接口重写getOrder方法\n\nspringboot利用了spring的什么特性进行封装的\nspring怎么解决循环依赖的\nhttps://javaguide.cn/system-design/framework/spring/spring-knowledge-and-questions-summary.html#spring-%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96%E4%BA%86%E8%A7%A3%E5%90%97-%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3\n\n三级缓存\n\n一级缓存:存放最终形态的Bean(已经实例化、属性填充、初始化)二级缓存:存放过渡Bean(半成品,属性未填充),存放的是三级缓存产生的对象三级缓存:存放ObjectFactory,调用其getObject方法生成对象。A依赖B,B依赖A。当创建A时,发现依赖B,然后会去创建B,然后发现依赖A,此时会从三级缓存中获得A的ObjectFactory,通过这个对象调用getObject方法获取A的前期暴露对象,没有初始化完成,但是在堆中存在内存地址了,然后将ObjectFactory从三级缓存移除放到二级缓存中。\n\n@Lazy注解\n\n如果一个bean被标记为懒加载,在容器启动时不会立即实例化,而是在第一次请求时才会创建。如果A和B产生了循环依赖,在A的构造器添加@Lazy后(延迟B的实例化),加载流程:\n\n首先创建A的Bean,发现需要注入B\n在A上标注了@Lazy注解,因此spring会去创建一个B的代理对象,将这个代理对象注入到A的B属性中\n之后实例化B,在注入B中的A属性时,A已经创建完毕,就可以将A注入进去。\n\nSpringBoot\n2.6之后官方不推荐编写有循环依赖的代码,建议减少不必要的循环依赖。\n","categories":["八股"],"tags":["Spring"]},{"title":"操作系统基础","url":"/2023/12/01/%E5%85%AB%E8%82%A1/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%9F%BA%E7%A1%80/","content":"\n注:大多数内容参考了JavaGuide\n操作系统\n虚拟内存\n\nhttps://javaguide.cn/cs-basics/operating-system/operating-system-basic-questions-02.html#%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98\n\n什么是虚拟内存,有什么用?\n\n虚拟内存是计算机系统内存管理的一个非常重要的技术。本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。总结来说,虚拟内存主要提供了下面这些能力:\n\n隔离进程:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。\n提升物理内存利用率:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。\n简化内存管理:进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。\n多个进程共享物理内存:进程在运行过程中,会加载许多操作系统的动态库。这些库对于每个进程而言都是公用的,它们在内存中实际只会加载一份,这部分称为共享内存。\n提高内存使用安全性:控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。\n提供更大的可使用内存空间:可以让程序拥有超过系统物理内存大小的可用内存空间。这是因为当物理内存不够用时,可以利用磁盘充当,将物理内存页(通常大小为\n4\nKB)保存到磁盘文件(会影响读写速度),数据或代码页会根据需要在物理内存与磁盘之间移动。\n\n\n没有虚拟内存有什么问题?\n\n\n用户程序可以访问任意物理内存,可能会不小心操作到系统运行必需的内存,进而造成操作系统崩溃,严重影响系统的安全。\n同时运行多个程序容易崩溃。比如你想同时运行一个微信和一个\nQQ 音乐,微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址\n1xxx 赋值,那么 QQ\n音乐对内存的赋值就会覆盖微信之前所赋的值,这就可能会造成微信这个程序会崩溃。\n程序运行过程中使用的所有数据或指令都要载入物理内存,根据局部性原理,其中很大一部分可能都不会用到,白白占用了宝贵的物理内存资源。\n\n\n虚拟地址和物理地址\n\n物理地址(Physical\nAddress)是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。程序中访问的内存地址不是物理地址,而是虚拟地址(Virtual\nAddress)。操作系统一般通过MMU(Memory Management\nUnit内存管理单元)将虚拟地址转换为物理地址,该过程称为地址翻译/地址转换(Address\nTranslation)。通过 MMU\n将虚拟地址转换为物理地址后,再通过总线传到物理内存设备,进而完成相应的物理内存读写请求。MMU翻译虚拟地址主要用三种方式:分段机制、分页机制和段页机制现代操作系统广泛采用分页机制。\n分段机制\n分段机制(Segmentation) 以段(—段\n连续\n的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段\nMAIN、子程序段 X、数据段 D 及栈段 S 等。\n段表有什么作用?地址翻译过程是什么?\n分段管理通过段表映射虚拟地址和物理地址。\n虚拟地址由两部分组成:\n\n段号:标识该虚拟地址属于整个虚拟地址空间中的哪一段\n段内偏移量:相对于该段起始地址的偏移量\n\n具体翻译过程:\n\nMMU首先解析虚拟地址中的段号\n通过段号从段表中取出对应的段信息(段表项)\n从段信息(段表项)中取出起始地址(物理地址)加上虚拟地址中的段内偏移量得到最终的物理地址\n\n\n段表中还保存了段长(检查虚拟地址是否超出范围)、段类型(代码段、数据段等)等信息。\n通过段号不一定找得到最终的物理地址,段表项可能不存在,因为:\n\n段表项被删除:软件错误、恶意行为等可能导致段表项被删除\n段表项还未创建:系统内存不足或者无法分配到连续的物理内存导致段表项无法被创建\n\n分段机制为什么会导致内存外部碎片\n外部内存碎片即段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段),造成物理内存资源利用率降低。\n分页机制\n分页机制(Paging)\n把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。现代操作系统广泛采用分页机制。\n注意:这里的页是连续等长的,不同于分段机制下不同长度的段。\n在分页机制下,应用程序虚拟地址空间中的任意虚拟页可以被映射到物理内存中的任意物理页上,因此可以实现物理内存资源的离散分配。分页机制按照固定页大小分配物理内存,使得物理内存资源易于管理,可有效避免分段机制中外部内存碎片的问题。\n页表作用,地址翻译过程\n分页管理通过页表映射虚拟地址和物理地址。分页机制中每个应用程序都会有一个对应的页表。虚拟地址由两部分组成:\n\n页号:通过虚拟页号找到对应的物理页号\n页内偏移量:物理页起始地址+页内偏移量=物理内存地址\n\n具体翻译过程:\n\nMMU首先解析得到虚拟地址的虚拟页号\n通过虚拟页号从页表中找到对应的物理页号(页表项)\n物理页号对应的物理页起始地址+虚拟地址的业内偏移量得到最终的物理地址。\n\n\n页表中还存有诸如访问标志(标识该页面有没有被访问过)、脏数据标识位等信息。\n通过虚拟页号一定要找到对应的物理页号吗?找到了物理页号得到最终的物理地址后对应的物理页一定存在吗?\n不一定!可能会存在页缺失。也就是说,物理内存中没有对应的物理页或者物理内存中有对应的物理页但虚拟页还未和物理页建立映射(对应的页表项不存在)。\n单级页表有什么问题?为什么需要多级页表\n只用一级页表的话,如果系统运行的程序多起来,页表的开销很大,占用空间,而且绝大多数程序可能只用到页表中的几项,其他的浪费了。采用二级页表的话,分为一级页表和二级页表,一级页表对应的二级页表是一对多的关系,二级页表是按需加载的,进而节省空间。\n.yzlkikjqlmns{zoom:67%;}\n\n多级页表属于时间换空间,利用增加页表的查询次数减少页表占用的空间。\nTLB作用?有TLB时候的地址翻译过程是什么\n为了提高虚拟地址到物理地址的转换速度,操作系统在页表方案基础之上引入了转址旁路缓存(Translation\nLookaside Buffer,TLB,也被称为快表)。\n\n一般来说,TLB属于MMU内部的单元,本质是一块高速缓存,缓存了虚拟页号和物理页号的映射关系。\n使用 TLB 之后的地址翻译流程是这样的:\n\n用虚拟地址中的虚拟页号作为 key 去 TLB 中查询;\n如果能查到对应的物理页的话,就不用再查询页表了,这种情况称为 TLB\n命中(TLB hit)。\n如果不能查到对应的物理页的话,还是需要去查询主存中的页表,同时将页表中的该映射表项添加到\nTLB 中,这种情况称为 TLB 未命中(TLB miss)。\n当 TLB\n填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。\n\n中断、软中断\n进程和线程的区别\n线程和协程的区别\n进程调度算法\n用户态和内核态的区别\n内存对齐\n","categories":["八股"],"tags":["操作系统"]},{"title":"计算机网络基础","url":"/2023/12/01/%E5%85%AB%E8%82%A1/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80/","content":"\n注:大多数内容参考了JavaGuide\n计算机网络\ndos攻击,有什么办法缓解\n\ntcp udp的区别\n\n是否面向连接:UDP\n在传送数据之前不需要先建立连接。而 TCP\n提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。\n是否是可靠传输:远地主机在收到 UDP\n报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP\n提供可靠的传输服务,TCP\n在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过\nTCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。\n是否有状态:这个和上面的“是否可靠传输”相对应。TCP\n传输是有状态的,这个有状态说的是 TCP\n会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此\n,TCP 需要维持复杂的连接状态表。而 UDP\n是无状态服务,简单来说就是不管发出去之后的事情了。\n传输效率:由于使用 TCP\n进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP\n低很多。\n传输形式:TCP 是面向字节流的,UDP\n是面向报文的。\n首部开销:TCP 首部开销(20 ~ 60 字节)比 UDP\n首部开销(8 字节)要大。\n是否提供广播或多播服务:TCP 只支持点对点通信,UDP\n支持一对一、一对多、多对一、多对多。\n\ntcp拥塞控制怎么实现\nhttps://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html#tcp-%E7%9A%84%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6%E6%98%AF%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0%E7%9A%84当网络拥塞时,减少数据的发送。TCP\n在发送数据的时候,需要考虑两个因素:一是接收方的接收能力,二是网络的拥塞程度。接收方的接收能力由滑动窗口表示,表示接收方还有多少缓冲区可以用来接收数据。网络的拥塞程度由拥塞窗口表示,它是发送方根据网络状况自己维护的一个值,表示发送方认为可以在网络中传输的数据量。发送方发送数据的大小是滑动窗口和拥塞窗口的最小值,这样可以保证发送方既不会超过接收方的接收能力,也不会造成网络的过度拥塞。发送方维护一个拥塞窗口。\n\n慢开始:发送方发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,所以从小逐渐增大发送窗口,初始为1,然后依次翻倍。\n拥塞避免:当发送窗口达到一定值,开始进行拥塞避免,缓慢增大发送窗口,每经过一个往返时间RTT把拥塞窗口加1。\n快重传和快恢复:如果接收方收到了没有按照顺序的数据段,会向发送方发送重复确认,如果发送方收到三次重复确认,则假定数据段丢失,并立即重传。\n\ntcp流量控制怎么实现\nhttps://javaguide.cn/cs-basics/network/tcp-reliability-guarantee.html#tcp-%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6\nTCP 连接的每一方都有固定大小的缓冲空间,TCP\n的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP\n使用的流量控制协议是可变大小的滑动窗口协议(TCP\n利用滑动窗口实现流量控制)。TCP\n利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。\n接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。\n浏览器输入网址按下回车后发生了什么\nTCP粘包\n","categories":["八股"],"tags":["计算机网络"]},{"title":"CSS基础","url":"/2024/11/23/%E5%89%8D%E7%AB%AF/CSS%E5%9F%BA%E7%A1%80/","content":"\n选择器\n\n通配选择器\n选择所有元素\n* { color: red;}\n元素选择器\n选择对应标签的所有元素\n/* 选择所有 h1 标签的元素 */h1 { color: blue;}/* 选择所有 p 标签 */p { color: red;}\n类选择器\n.test { color: red;}\n<!-- 设置 div 标签选择 test 样式 --><div class=\"test\">abctest</div><p class=\"test\">abc</p>\nID 选择器\n/* id选择器只能是唯一的 */#test { color: red;}\n<!-- id必须是唯一的 --><div id=\"test\">abc</div>\n并集选择器\n.test { font-size: 12px;}.test2 { font-size: 20px;}/* 在 test 和 test2 样式上继续添加样式 */.test,.test2 { color: red;}\n<div class=\"test\">abc</div><p class=\"test2\">xxx</p>\n交集选择器\n注意点:\n\n交集选择器,如果有元素选择器,元素必须在开头\n不可能出现两个元素选择器\n\n.test { color: red;}/* 交集选择器,元素选择器和类选择器同时满足条件,p 元素且类样式是 test */p.test { font-size: 12px;}\n<div class=\"test\">abc</div><p class=\"test\">xxx</p>\n后代选择器\n/* div 元素内的所有的 p 元素 */div p {}\n<!-- div 内所有的 p 元素都会选择 --><div> <p></p> <div> <p></p> </div></div>\n子代选择器\n/* div 元素内的所有的直接子元素 p */div > p {}\n<div> <!-- 选择这个 --> <p></p> <div> <!-- 不选择这个 --> <p></p> </div></div>\n兄弟选择器\n/* 选择和 div 紧紧相邻的(必须是挨着,如果挨着的不是 p,则不会选择) 后边的 一个兄弟 p 元素 */div + p {}/* 选择 div 后边的 所有兄弟 p 元素 */div ~ p {}\n<div></div><p></p><p></p>\n属性选择器\n/* 选择有 title 属性的 */[title] {}/* 选择有 title 属性的 且 属性值为 abc */[title='abc'] {}/* 选择 title 属性以 ab 开头的 */[title^='ab'] {}/* 选择 title 属性以 bc 结尾的 */[title$='bc'] {}/* 选择 title 属性只要有 b 即可,不少于 1 个 b */[title*='b'] {}\n<div title=\"abc\"></div>\n伪类选择器\na:link {}a:visited {}/* 选择 当鼠标悬停在 div 元素上时 */div:hover {}a:active {}/* 按照结构找 *//* 选择 div 元素的所有儿子 p 元素,且 这个 p 元素是 它 父级元素 的第一个儿子 *//* 如果第一个元素是 div ,则不会选择任何元素 */div > p:first-child {}/* 按照元素类型找 *//* 选择 div 元素的所有儿子 p 元素的第一个 p 元素 */div > p:first-of-type {}/* nth-child(an+b) 括号中必须是 an+b 的形式,其中 n 的范围是 0 到 正无穷 *//* 选择所有满足 an+b 条件的元素 */div > p:nth-child(an + b) {}\n<div> <!-- <div></div> --> <p></p> <p></p></div>\n伪元素选择器\n/* 在 p 元素的内容前 插入内容 abc */p::before { content: 'abc';}/* 在 p 元素内容之后插入 */p::after {}/* 选择第一行 */p::first-line {}\n选择器优先级\n如果选择器有多种组合起来,则选择器优先级按照如下规则\na, b, c\n\na: ID 选择器的个数\nb: 类、伪类、属性选择器的个数\nc: 元素、伪元素选择器的个数\n\n依次按照a, b, c的大小判断,大的优先级高,如果相同,则按照“后来者居上”覆盖之前的样式。\n如果有行内样式,则优先级高于 css 样式。\n如果属性后有\n!important,则这个样式优先级最高(也高于行内样式)。\n.test { color: red !important;}\n盒子模型\n元素显示模式\n\n块级元素\n独占一行,默认宽度撑满父元素,默认高度由内容撑开,可以通过CSS设置宽高。\n行内元素\n不独占一行,默认宽高由内容撑开,无法通过CSS设置宽高。\n行内块元素\n不独占一行,默认宽高由内容撑开,可以通过CSS设置宽高。\n\n盒子组成部分\ncontent内容区,padding内边距(补白),border边框,margin外边距。\n\n外边距 margin\n\n子元素的margin计算是从父元素的content区域开始算起。\nmargin-top, margin-left会影响自身的位置,margin-bottom, margin-right会影响后续兄弟元素的位置。\n行内元素可以设置margin-left, margin-right可以设置,但是margin-top, margin-bottom设置无效。\nmargin: 0 auto水平居中,margin-left: auto表示离左边“能有多远有多远”,margin-right同理。\nmargin可以设置负值,则向相反位置偏移。\n\nmargin 塌陷\n定义:第一个子元素的margin-top会作用在父元素上,最后一个子元素的margin-bottom会作用在父元素上。\n解决办法:\n\n父元素设置不为 0 的padding。\n父元素设置不为 0 的border。\n父元素这设置overflow: hidden。\n\nmargin 合并\n上边兄弟元素的margin-bottom和下边兄弟元素的margin-top会合并,取一个最大的值,而不是相加。\n无需刻意解决,设置时只给一个元素设置margin-top或者margin-bottom即可。\n内容溢出\noverflow: hidden:内容溢出后直接隐藏。\noverflow: auto:内容溢出后添加滚动条可以滚动。\noverflow: scroll:内容不管是否溢出都添加滚动条。\n元素显示与隐藏\ndisplay: none:元素不显示,也不会占位。\nvisibility: hidden:元素不显示,但是会占位。\n定位\n如果开启了相对定位或者绝对定位,层级比普通的元素高\n\n相对定位\nposition: relative相对定位,相对于元素设置相对定位前(原来的位置)的位置。可以设置top, right, bottom, left。\n设置了相对定位的元素,并没有脱离文档流。\n绝对定位\nposition: absolute绝对定位,脱离文档流,相对于祖先元素中第一个不为\nstatic(默认定位)定位的元素\n固定定位\nposition: fixed相对于浏览器窗口定位,脱离文档流\n\nCSS3 新增\n新增长度单位\nvw:视口的宽度\nvh:视口的高度\nvmax:视口宽高的最大值作为单位(了解即可)\nvmin:视口宽高的最小值作为单位(了解即可)\nrem:根元素字体大小的倍数,只与根元素字体大小有关。\n新增盒子模型属性\n\nbox-sizing: border-box\n设置该属性(怪异盒模型)后,设置的width, height为盒子的总宽高,并非内容区的宽高(不设置该属性时为内容区的宽高)\nresize\n使用的前提是必须有overflow属性\nresize: horizontal:水平方向上的改变大小\nresize: vertical:垂直方向上改变大小\nbox-shadow\n盒子阴影\n.test { /* 水平位置,垂直位置,阴影颜色或者模糊程度,(阴影外沿),阴影颜色,(内阴影) */ box-shadow: 10px 15px blue; box-shadow: 10px 15px 10px; /* 常见写法 */ box-shadow: 10px 15px 10px red; box-shadow: 10px 15px 10px 12px green; box-shadow: 10px 15px 10px 12px green inset;}\nbox-shadow: h-shadow v-shadow blur spread color inset\n\n\n\n值\n含义\n\n\n\n\nh-shadow\n水平阴影的位置,必填,可以为负值\n\n\nv-shadow\n垂直阴影的位置,必填,可以为负值\n\n\nblur\n可选,模糊距离\n\n\nspread\n可选,阴影外延值\n\n\ncolor\n可选,阴影颜色\n\n\ninset\n可选,外部阴影改为内部阴影\n\n\n\nopacity\n不透明度,范围[0, 1],1 为不透明,0 为全透明\n\n新增背景属性\n新增边框属性\n新增渐变\n2D 变换\n和transform相关的都不可以使用在行内元素上。\n\n位移\n.test { /* x轴方向偏移 50px */ transform: translateX(50px); /* 如果写百分比,则长度参考的自身元素长的50%,(注意和其他属性的区别,其他属性时参考父元素的) */ transform: translateX(50%); /* y轴方向 */ transform: translateY(50px);}\n定位配合位移实现水平垂直居中。\n🍰如果有父元素,则需要设置父元素为相对定位\n.test { position: absolute; /* 相对于父元素偏移 50% */ top: 50%; left: 50%; /* 自身元素长的 50% */ transform: translate(-50%, -50%);}\n缩放\n.test { /* 1,不变,小于 1 缩小,大于 1 扩大,如果小于 0 则可以认为做了翻转,但不推荐写小于 0 */ transform: scale(0.5, 1.5);}\ntips:可以实现文字小于12px的效果\n旋转\n❗元素旋转后坐标系也跟着发生了旋转,涉及多重变换时,尽量将旋转放在最后。\n.test { /* 沿着元素中心顺时针旋转 30deg */ transform: rotate(30deg); /* rotateZ 属于 2D 的范围,z轴相当于从屏幕里射出来的方向,对于屏幕 2D 的旋转,则是按照 z轴 旋转 */ transform: rotateZ();}\n扭曲\n\n使用的很少\n\n.test { transform: skewX(30deg);}\n变换原点\n.test { /* 设置变换原点在元素的什么位置 */ transform-origin: left top; /* 设置具体的原点 */ /* 如果只写一个值,另一个值则取中间 */ transform-origin: 50px 50px;}\n变换原点对旋转、缩放有影响。\n\n3D 变换\n过渡\n\n基本使用\n可以用数字表示的属性都可以进行过渡。\n.box { width: 200px; height: 200px; background-color: red; /* 设置哪个属性具有 过渡 效果 */ /* all,过渡所有能过渡的属性,不写默认是 all */ transition-property: width, height; /* 设置 过渡 持续时间,单位 ms 或 s */ transition-duration: 2s, 1s;}.box:hover { width: 500px; height: 400px;}\n高级用法\n.test { /* 过渡发生的延迟 */ transition-delay: 2s; /* 过渡动画, ease 平滑,默认值;linear 线性;ease-in 先慢后快;ease-out 先快后慢;step-start 不考虑过渡时间,直接到终点 */ /* step-end 过渡时间结束了直接到达终点;steps(20, start[end]) 分步过渡; cubic-bezier(0.88, 1.03, 0.78, 1.24) 贝塞尔曲线 */ transition-timing-function: ease; transition-timing-function: cubic-bezier(0.88, 1.03, 0.78, 1.24);}\n复合属性\n.test { /* duration, property, delay, timing-function */ /* 两个时间有顺序要求,其他的没有顺序要求,一般延迟时间不写,采用默认0s */ transition: 3s all 0.5s linear;}\n\n动画\n\n基本使用\n.test { animation-name: moveRight; /* 动画持续时间 */ animation-duration: 2s;}/* 定义一组关键帧 */@keyframes moveRight { from { } to { transform: translate(900px); }}/* 另一种定义方式,使用百分比更精细的控制 */@keyframes moveRight { 0% { } 50% { } 100% { }}\n其他属性\n.test { animation-name: moveRight; /* 动画持续时间 */ animation-duration: 2s; animation-delay: 0.5s; /* 设置动画方式,类比于 过渡 的属性 */ animation-timing-function: linear; /* 动画播放的次数 */ animation-iteration-count: infinite; /* 修改动画方向 */ animation-direction: reverse /* 反转 */; animation-direction: alternate /* 往返运动 */; /* 动画以外的状态(不发生动画时的状态) */ animation-fill-mode: forwards / backwards; /* 动画播放状态 */ animation-play-state: paused;}/* 定义一组关键帧 */@keyframes moveRight { from { } to { transform: translate(900px); }}\n复合属性\n.test { /* 只有两个时间有顺序,第一个时间是持续时间,第二个是延迟 */ animation: moveRight 2s 0.5s linear 2 alternate-reverse forwards;}.test:hover { /* 暂停属性单独使用 */ animation-play-state: paused;}\n动画和过渡的区别\n\n过渡需要一定的触发条件,动画不需要\n过渡只关注开始和结束,无法关注中间某个过程。\n\n\n多列布局\n.test { /* 设置 3 列 多列布局 */ column-count: 3; /* 指定每一列宽度 */ column-width: 200px; /* 可以同时指定列数和列宽,如果结果不同,则列数少的优先级高 */ columns: 3 220px; /* 设置列之间的间隙 */ column-gap: 20px; /* 设置列之间的分割线 */ column-rule-width: 2px; column-rule-style: dashed; column-rule-color: red; /* 分割线复合属性 */ column-rule: 2px dashed red; /* 指定标题文本跨所有列,只能是 none / all,默认值为 none */ column-span: all;}\n伸缩盒模型\n容器和项\n父元素设置display: flex;后,变为一个“伸缩容器”,其所有的子元素的都是“伸缩项”,并且所有的子元素都会块状化变为块元素。\n主轴方向换行方式\n.test { /* 设置主轴方向 */ flex-direction: row; flex-direction: row-reverse; flex-direction: column; flex-direction: column-reverse; /* 默认不换行 */ flex-wrap: nowrap; /* 按照交叉轴方向换行 */ flex-wrap: wrap; /* 按照交叉轴反方向换行 */ flex-wrap: wrap-reverse;}\n主轴对齐方式\n.test { /* 默认,主轴起始位置对齐 */ justify-content: flex-start; /* 主轴结束位置对齐 */ justify-content: flex-end; /* 居中对齐 */ justify-content: center; /* 项目均匀分布在一行中,每个项目两侧空出的距离相等,项目之间的距离是两侧的二倍, */ justify-content: space-around; /* 项目均匀分布在一行中,项目与项目之间距离相等,两侧的项目紧贴边缘 */ justify-content: space-between; /* 项目均匀分布在一行中 */ justify-content: space-evenly;}\n侧轴对齐方式\n单行\nalign-items对每一行的项起作用\n.test { /* 交叉轴起始位置对齐 */ align-items: flex-start; /* 交叉轴中间位置对齐 */ align-items: center; align-items: flex-end; /* 如果所哟伸缩项都没有高度,就撑满整个容器,默认值 */ align-items: stretch;}\n多行\nalign-content对多行整体起作用\n.test { /* 交叉轴的起始位置对齐 */ align-content: flex-start; align-content: flex-end; /* 交叉轴居中 */ align-content: center; /* 同主轴 */ align-content: space-around; align-content: space-between; align-content: space-evenly; /* 同上,默认值 */ align-content: stretch;}\n伸缩项目在主轴上的基准长度\n.test { /* 设置伸缩项在主轴上的长度,如果主轴是横向,则 width 失效,纵向则 height 失效 */ /* 默认值 auto */ flex-basis: 300px;}\n伸缩性\n.test { /* 分剩余空间的比例,权重 */ /* 如果为 0,则有剩余空间也不拉伸,默认值是 0 */ flex-grow: 1;}\n.test { /* 如果设置了 wrap 换行,则不会压缩 */ /* 当空间不够了,设置伸缩项的缩小比例 */ flex-shrink: 1;}\nflex-shrink具体计算公式:flex 布局中\nflex-grow 与 flex-shrink 的详细计算方式_flex grad-CSDN 博客\n两个元素,宽度分别为w1, w2,flex-shrink分别为f1, f2,溢出(元素超出容器的部分)的宽度为o\n\n则第一个元素缩小,第二个元素缩小\n如果f1 + f2 < 1,则最后计算缩小长度时的o为\n复合属性\n.test { /* flex-grow flex-shrink flex-basis */ flex: 1 1 200px; /* flex: 1 1 auto; 的简写形式 */ flex: auto; /* flex: 1 1 0; 的简写形式 */ flex: 1; /* flex: 0 0 auto; 的简写形式 */ flex: none; /* 默认值 */ flex: 0 1 auto; /* 可以简写为:flex: 0 auto; */}\n排序\n.test { /* 越小的越靠前 */ order: -1; /* 设置某个元素单独的对齐方式 */ align-self: flex-end / center;}\n媒体查询\n媒体类型\n媒体查询样式没有提高优先级,媒体查询样式之间和正常的样式有顺序要求,因此一般先写正常样式,最后写媒体查询样式。\n/* 媒体查询 查询到是打印机 / 预览打印 时,设置样式 */@media print {}/* 只有在屏幕上才设置的样式 */@media screen {}\n媒体特性\n/* 检测到视口宽度为 800px 时,应用样式 */@media (width: 800px) {}/* 检测到视口宽度 小于等于 700px 时应用样式 */@media (max-width: 700px) {}/* 检测到视口宽度 大于等于 900px 时应用样式 */@media (min-width: 900px) {}/* 设备宽度如果是 1920px 应用样式(最新已弃用这个属性) */@media (device-width: 1920px) {}\n其他特性:\n\nmax-device-width, min-device-width\norientation检测视口的旋转方向(是否横屏),portrait纵向,高度大于等于宽度;landscape横向,宽度大于高度。\n\n运算符\n/* 同时满足两个条件,小于等于 700px 且 大于等于 600px */@media (max-width: 700px) and (min-width: 600px) {}/* 或,满足一个条件即可 */@media (max-width: 700px) or (min-width: 600px) {} /* 新语法,推荐使用 */@media (max-width: 700px), (min-width: 600px) {}/* 否定运算符,不是屏幕 */@media not screen {}/* 肯定运算符,用处不大,主要可以用于处理 IE 的兼容问题 */@media only screen {}\nBFC\n在一定条件下可以开启BFC,开启 BFC\n可以解决哪些问题:\n\n元素开启 BFC 后,子元素不会产生margin塌陷问题\n元素开启 BFC 后,自己不会被其他浮动元素覆盖\n元素开启 BFC 后,如果其子元素浮动,元素自身高度也不会塌陷\n\n如何开启BFC\n\n根元素\n浮动元素\n绝对定位、固定定位的元素\n行内块元素\n表格单元格:table, thead,\ntbody, tfoot, th,\ntd, tr, caption\noverflow的值不为visible的块元素\n伸缩项目\n多列容器(column-count)\ncolumn-span为all的元素\ndisplay的值设为flow-root\n\n一些值得关注的“坑”\n文字超长省略\n通常情况下设置flex: 1来让元素达到自适应的效果,如自动填满剩余空间。\n但在遇到文本超长的时候,设置flex: 1却不能保证文本元素省略,会挤占后边的空间。这时候通常需要设置min-width: 0来解决。\nW3C\n中:主轴上Flex元素的overflow属性是visible时,主轴上Flex元素的最小尺寸(min-size)将会指定一个自动的最小尺寸,通常是浏览器会将其设置为max-content。因此需要显式设置最小尺寸,即min-width: 0 / min-height: 0。\n当不显式设置min-width时,默认情况下:\nmin-width: 0,但当作为Flex元素时:\nmin-width: max-content。\n","categories":["前端"],"tags":["CSS"]},{"title":"React Native环境搭建","url":"/2023/12/01/%E5%89%8D%E7%AB%AF/React%20Native%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/","content":"\n环境搭建\nNode和JDK\n\nNode版本\n18\nJDK版本\n\n\n\n\nReact Native版本\nJDK版本\n\n\n\n\n 0.73\njdk17\n\n\n 0.73\njdk11\n\n\n 0.67\njdk8\n\n\n\n\nyarn安装npm install -g yarn,Yarn是 Facebook 提供的替代\nnpm的工具 ## Android开发环境\n下载安装Android Studio\n安装Android SDKSDK Platforms:\nAndroid API 33SDK Tools:\nAndroid SDK Build-Tools 33.0.0、**Android SDK Command-line Tools 12.0**\n设置环境变量默认情况下Android SDK安装在C:\\Users\\<username>\\AppData\\Local\\Android\\Sdk中,可以自行修改。添加环境变量:设置ANDROID_HOME为C:\\Users\\<username>\\AppData\\Local\\Android\\Sdk在Path环境变量中添加:%ANDROID_HOME%\\platform-tools和**%ANDROID_HOME%\\cmdline-tools\\12.0\\bin**\n## 准备Android设备\n使用Android真机\n使用Android模拟器使用Android Studio创建虚拟设备\n检查设备是否存在使用adb devices检查\n\n ## 创建模板项目\n\n新建项目npx react-native init TemplateProject\n编译运行 cd TemplateProjectyarn android ## 遇到的问题\n环境检查npx react-native doctor命令检查当前项目中的环境是否完备。\n\n\n\nimage.png\n\n\n识别不到Android SDK安装了SDK并且配置了环境变量,但是无法识别。需要安装Android SDK Command-line Tools 12.0,并配置环境变量。\nAndroid SDK打包失败https://stackoverflow.com/questions/68387270/android-studio-error-installed-build-tools-revision-31-0-0-is-corrupted\n\n修改%ANDROID_HOME%/build-tools/33.0.0目录中的d8.bat,修改为dx.bat\n修改%ANDROID_HOME%/build-tools/33.0.0/lib目录中的d8.jar,修改为dx.jar\n# 组件库 经过调研,可以选取React Native Elements组件库(链接),该组件库在github有24k\nstars,属于很流行的组件库。 ## 安装\n这里选取React Native CLI方式安装。\n\n安装组件库\n\nyarn add @rneui/themed @rneui/base\n\n安装依赖\n\nyarn add react-native-safe-area-context\nyarn add react-native-vector-icons\n\n设置react-native-vector-icons\n\n推荐使用Gradle设置,在android/app/build.gradle文件中添加语句apply from: file(\"../../node_modules/react-native-vector-icons/fonts.gradle\")\n# React Navigation 安装依赖 yarn add @react-navigation/native// 如果是纯react native项目安装该依赖yarn add react-native-screens react-native-safe-area-context\n在android/app/src/main/java/<your package name>包下的MainActivity类中添加如下代码:\nclass MainActivity: ReactActivity() { // ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(null) } // ...}\n同时在这个文件最开头导入:import android.os.Bundle;在App组件最外侧使用NavigationContainer标签包裹\n## Stack\n安装yarn add @react-navigation/stack安装依赖yarn add react-native-gesture-handler使用在App.js或者index.js文件最开头引用:import 'react-native-gesture-handler';\n\n\n","categories":["技术"],"tags":["rn"]},{"title":"ReactNative基础学习","url":"/2023/12/01/%E5%89%8D%E7%AB%AF/ReactNative%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0/","content":"1 环境搭建\n1.1 Node和JDK\n\nNode版本 \n18\nJDK版本\n\n\n\n\nReact Native版本\nJDK版本\n\n\n\n\n 0.73\njdk17\n\n\n 0.73\njdk11\n\n\n 0.67\njdk8\n\n\n\n\n\nyarn安装\nnpm install -g yarn,Yarn是 Facebook 提供的替代\nnpm的工具\n\n1.2 Android开发环境\n\n下载安装Android Studio\n安装Android SDK\nSDK Platforms: Android API 33\nSDK Tools:\nAndroid SDK Build-Tools 33.0.0、Android SDK Command-line Tools 12.0\n设置环境变量\n默认情况下Android SDK安装在C:\\Users\\<username>\\AppData\\Local\\Android\\Sdk中,可以自行修改。\n添加环境变量:设置ANDROID_HOME为C:\\Users\\<username>\\AppData\\Local\\Android\\Sdk\n在Path环境变量中添加:%ANDROID_HOME%\\platform-tools和%ANDROID_HOME%\\cmdline-tools\\12.0\\bin\n\n1.3 准备Android设备\n\n使用Android真机\n使用Android模拟器\n使用Android Studio创建虚拟设备\n检查设备是否存在\n使用adb devices检查\n\n\n1.4 创建模板项目\n\n新建项目\nnpx react-native init TemplateProject\n编译运行\ncd TemplateProjectyarn android\n\n.fulbhhubhxqb{zoom:33%;}\n\n1.5 遇到的问题\n\n环境检查\nnpx react-native doctor命令检查当前项目中的环境是否完备。\n\n识别不到Android SDK\n安装了SDK并且配置了环境变量,但是无法识别。\n需要安装Android SDK Command-line Tools 12.0,并配置环境变量。\nAndroid SDK打包失败\nhttps://stackoverflow.com/questions/68387270/android-studio-error-installed-build-tools-revision-31-0-0-is-corrupted\n\n修改%ANDROID_HOME%/build-tools/33.0.0目录中的d8.bat,修改为dx.bat\n修改%ANDROID_HOME%/build-tools/33.0.0/lib目录中的d8.jar,修改为dx.jar\n\n\n","tags":["React Native"]},{"title":"React基础学习","url":"/2024/09/01/%E5%89%8D%E7%AB%AF/React%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0/","content":"\n1 jsx语法规则\n\n定义虚拟dom时,不要写引号。\nconst vdom = <h1>lishan</h1>\n标签中引入js表达式要用{}\n样式的类名指定不要用class,要用className\n内联样式要用style={{key: value}}的形式,第一层括号表示引用js表达式,第二层括号表示是一个对象\n只有一个根标签\n标签必须闭合\n标签首字母\n\n若小写字母开头,则将该标签转为html同名元素,若html无同名元素,则报错\n若大写字母开头,react渲染对应的组件,若组件没定义,则报错\n\n\n2 组件\n2.1 函数式组件\nfunction MyComponent() { console.log(this) // this是undefined,因为babel编译后开启了严格模式,不能指向window return <h1>函数式定义的组件</h1>}// test为定义的id为test的容器ReactDOM.render(<MyComponent/>, docutment.getElementById(\"test\"))\n\nReact解析组件标签,找到了MyCompoent组件。\n发现组件是函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,然后渲染到页面中。\n\n2.2 类式组件\nclass MyComponent extends React.Component { render() { // render中的this是指向MyComponent的实例对象 return <h2>类式组件</h2> }}// 此render和类中的render不一样,只是名字相同ReactDOM.render(<MyComponent/>, docutment.getElementById(\"test\"))\n\nReact解析组件标签,找到了MyCompoent组件。\n发现组件是类定义的,随后new出该类的实例,并通过该实例调用原型的render方法。\n将类中render返回的虚拟DOM转为真实DOM,然后渲染到页面中。\n\n3 组件实例的三大核心属性\n3.1 state\nclass Weather extends React.Component { constructor(props) { super(props) // state必须是对象形式 this.state = {weather: '炎热'} } render() { // 事件绑定onClick,和原生的onclick有所不同,同时要注意{}中传入的是js表达式,不能写demo(),因为这样会直接调用demo()将返回值传给onClick,应该写demo传函数名 return <h2 onClick={demo}>天气:{this.state.weather}</h2> }}// 此render和类中的render不一样,只是名字相同ReactDOM.render(<Weather/>, docutment.getElementById(\"test\"))function demo() { console.log(\"标题被点击了\")}\n解决changeWeather中this指向问题\nclass Weather extends React.Component { constructor(props) { super(props) // state必须是对象形式 this.state = {weather: '炎热'} // 解决changeWeather中的this指向问题 this.changeWeather = this.changeWeather.bind(this) } // 调用1+n次,初始化调用1次,后续的是修改state后重新渲染 render() { // this.changeWeather并不是调用函数,只是赋值,通过onClick调用函数中的this并不是指向实例对象 return <h2 onClick={this.changeWeather}>天气:{this.state.weather}</h2> } changeWeather() { // 自定义方法中的this并不是指向Weather实例对象,因为不是通过实例对象调用 // changeWeather是作为onClick的回调,所以不是通过实例调用的,而是直接调用,类中的方法开启了局部严格模式,所以changeWeather中的this为undefined console.log(\"标题被点击了\") // 这是错误写法,状态不可直接更改,直接修改不会影响到页面 // this.state.weather = \"凉爽\" // 需要使用this中的setState函数修改,传入对象,指定需要修改的属性 this.setState({weather: \"很热\"}) }}// 此render和类中的render不一样,只是名字相同ReactDOM.render(<Weather/>, docutment.getElementById(\"test\"))\nstate简化方式\nclass Weather extends React.Component { state = {weather: \"炎热\"} render() { return <h2 onClick={this.changeWeather}>天气:{this.state.weather}</h2> } // 箭头函数中没有this,如果使用this,会将this赋值给箭头函数外侧的函数的this指向 changeWeather = () => { console.log(\"标题被点击了\") this.setState({weather: \"很热\"}) }}ReactDOM.render(<Weather/>, docutment.getElementById(\"test\"))\n3.2 props\n在组件标签中写属性和值,会传入到对象中的props中。\nclass Weather extends React.Component { render() { return <h2>天气:{this.props.name}</h2> }}ReactDOM.render(<Weather name=\"炎热\"/>, docutment.getElementById(\"test\"))\n批量props传入\nclass Weather extends React.Component { render() { return <h2>天气:{this.props.name}</h2> }}const w = {name: \"凉爽\"}// 其中...w相当于name={w.name}// ...展开运算符展开对象只适用于标签属性的传递,别的情况不适用ReactDOM.render(<Weather {...w}/>, docutment.getElementById(\"test\"))\nprops的限制\nclass Person extends React.Component { render() { return ( <h2>年龄:{this.props.age}</h2> <h2>名字:{this.props.name}</h2> ) }}// 设置规则Person.propTypes = { // name: React.PropTypes.string // React16及以后新版本不支持这个写法 name: PropTypes.string.isRequired, // name属性为字符串类型且必须 age: PropTypes.number // age属性为number,如果不传,默认值为18 speak: PropTypes.func // 注意不要用function,因为function和js的function关键字冲突,因此改为func}Person.defaultProps = { age: 18 // 默认值}// 传入的是18的字符串// ReactDOM.render(<Person age=\"18\"/>, docutment.getElementById(\"test\"))// 传入的是18的number类型ReactDOM.render(<Person age={18} speak={speak}/>, docutment.getElementById(\"test\"))function speak() { console.log(\"说话\")}\nprops的简写\nclass Person extends React.Component { render() { return ( <h2>年龄:{this.props.age}</h2> <h2>名字:{this.props.name}</h2> ) } // 使用static给类自身加属性,需要使用static,否则是给实例对象加属性 static propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number, speak: PropTypes.func } static defaultProps = { age: 18 }}ReactDOM.render(<Person age={18} speak={speak}/>, docutment.getElementById(\"test\"))function speak() { console.log(\"说话\")}\n函数式组件使用props\nfunction MyComponent(props) { return <h1>{props.name}</h1>}MyComponent.propTypes = { name: PropTypes.string.isRequired}ReactDOM.render(<MyComponent name=\"lishan\"/>, docutment.getElementById(\"test\"))\n3.3 refs\n字符串形式的refs(不推荐使用,大量使用会有较大的效率问题)\nclass Demo extends React.Component { testRef = () => { console.log(this.refs.testh2) } render() { return ( \t<h2 ref=\"testh2\" onClick={this.testRef}>test</h2> ) }}\n回调函数形式的refs\nclass Demo extends React.Component { testRef = () => { console.log(this.testh2) } render() { return ( // ref中的是回调函数,React会自动调用 \t<h2 ref={c => this.testh2 = c} onClick={this.testRef}>test</h2> ) }}\n回调函数形式的refs中的回调函数执行次数:\n如果ref回调函数以内联函数方式定义,在更新过程中会执行两次,第一次传入参数为null,然后第二次传入参数DOM元素。通过将ref的回调函数定义成class的绑定函数的方式避免上述问题。但这种影响不是很大,简单起见可以写成内联函数样式\nclass Demo extends React.Component { testRef = () => { console.log(this.testh2) } saveTest = (c) => { this.testh2 = c } render() { return ( // jsx中注释标签,需要采用这种方式,转成js再进行注释 {/* <h2 ref={c => this.testh2 = c} onClick={this.testRef}>test</h2> */} <h2 ref={this.saveTest} onClick={this.testRef}>test</h2> ) }}\ncreateRef的使用(当前React官方最推荐的方式)\nclass Demo extends React.Component { // 只能存储一个节点标签 testh2 = React.createRef() testRef = () => { console.log(this.testh2.current) } render() { return ( \t<h2 ref={this.testh2} onClick={this.testRef}>test</h2> ) }}\n❗不要过度使用refs\n4 React事件处理\n\n通过onXxx属性指定事件处理函数\n\nReact使用的是自定义(合成)事件,而不是原生的DOM事件\nReact中的事件是通过委托方式处理的(委托给组件最外层的元素)\n\n通过event.target得到发生事件的DOM元素对象(可以省略部分ref的使用)\n\nclass Demo extends React.Component { testRef = (event) => { console.log(event.target) } render() { return ( \t<h2 onClick={this.testRef}>test</h2> ) }}\n5 受控组件和非受控组件\n5.1 非受控组件\n数据“现用现取”\nclass Demo extends React.Component { testNode = React.createRef() // 需要使用时通过ref获取节点从而获取值 render() { return ( \t<input ref={this.testNode} type=\"text\"/> ) }}\n5.2 受控组件\n例如,input输入的数据,每次改变时将数据维护到state中,当需要时直接从state中获取\nclass Demo extends React.Component { state = { username: \"default\" } saveInput = (event) => { this.setState({username: event.target.value}) } render() { return ( \t<input onChange={this.saveInput} type=\"text\"/> ) }}\n🎉\n推荐使用受控组件,减少ref的使用。\n6 高阶函数和柯里化\n6.1 高阶函数\n如果需要实时保存很多输入的数据,并且保存的方式都相同,那么下面这种方式会很冗余麻烦。\nclass Demo extends React.Component { state = { username: \"default\", password: \"password\" } saveUsername = (event) => { this.setState({username: event.target.value}) } savePassword = (event) => { this.setState({password: event.target.value}) } render() { return ( <div> <input onChange={this.saveUsername} type=\"text\"/> <input onChange={this.savePassword} type=\"password\"/> </div> ) }}\n可以采用给方法传参方式标识需要保存的数据是哪个类型(属性)\nclass Demo extends React.Component { state = { username: \"default\", password: \"password\" } // 柯里化 saveInfo = (dataInfo) => { // 这里返回一个函数给React调用 return (event) => { // 必须加上[]读出dataInfo变量的值,否则会认为dataInfo是一个新的属性等价于{\"dataInfo\": event.target.value} this.setState({[dataInfo]: event.target.value}) } } render() { return ( <div> <input onChange={this.saveInfo(\"username\")} type=\"text\"/> <input onChange={this.saveInfo(\"password\")} type=\"password\"/> </div> ) }}\n高阶函数的定义:\n\n若函数接收的参数是一个函数,那该函数是高阶函数\n若函数返回值是一个函数,该函数是高阶函数\n\n6.2 柯里化\n函数的柯里化:通过函数调用继续返回函数的形式,实现多次接收参数最后统一处理的函数编码形式。\n// 柯里化saveInfo = (dataInfo) => { // 这里返回一个函数给React调用 return (event) => { // 必须加上[]读出dataInfo变量的值,否则会认为dataInfo是一个新的属性等价于{\"dataInfo\": event.target.value} this.setState({[dataInfo]: event.target.value}) }}\n不使用柯里化的方式:\nclass Demo extends React.Component { state = { username: \"default\", password: \"password\" } saveInfo = (dataInfo, event) => { this.setState({[dataInfo]: event.target.value}) } render() { return ( <div> <input onChange={(event) => {this.saveInfo(\"username\", event)}} type=\"text\"/> <input onChange={(event) => {this.saveInfo(\"password\", event)}} type=\"password\"/> </div> ) }}\n7 生命周期\n\n7.1 卸载组件\nReactDOM.unmountComponentAtNode(document.getElementById(\"test\"))\n7.2 组件将要挂载周期\nclass Demo extends React.Component { componentWillMount() { } // 初始化渲染,组件更新后调用 render() { return ( <div>test</div> ) }}\n7.3 组件完成挂载周期(常用)\nclass Demo extends React.Component { // 初始化渲染,组件更新后调用 render() { return ( <div>test</div> ) } // 组件挂载时只调用一次 componentDidMount() { }}\n7.4 组件将要卸载周期(常用)\nclass Demo extends React.Component { // 初始化渲染,组件更新后调用 render() { return ( <div>test</div> ) } componentWillUnmount() { }}\n7.5 是否应该组件更新周期\nsetState()直接触发该周期\nclass Demo extends React.Component { // 该钩子必须返回true或者false,返回true表示页面应该更新,否则不更新。 // React默认实现该钩子返回true shouldComponentUpdate() { return true } // 初始化渲染,组件更新后调用 render() { return ( <div>test</div> ) }}\n7.6 组件将要更新周期\nforceUpdate()直接触发该周期\nclass Demo extends React.Component { componentWillUpdate() { } // 初始化渲染,组件更新后调用 render() { return ( <div>test</div> ) }}\n7.7 组件完成更新周期\nclass Demo extends React.Component { // 初始化渲染,组件更新后调用 render() { return ( <div>test</div> ) } componentDidUpdate(preProps, preState) { }}\n7.8 组件将要接收Props周期\nclass Demo extends React.Component { state = {name: \"lishan\"} change = () => { // 执行后会触发state修改,父组件重新渲染(第一次渲染不会执行)后会执行子组件componentWillReceiveProps钩子及后续钩子 this.setState({name: \"test\"}) } // 初始化渲染,组件更新后调用 render() { return ( <div> <B name={this.state.name}/> <button onClick={this.change}>修改</button> </div> ) }}class B extends React.Component { // 参数为传入的props() componentWillReceiveProps(props) { } render() { return <div>{this.props.name}</div> }}\n7.9 新旧生命周期对比\n新生命周期图:\n.aodcvrzzqvsp{zoom:80%;}\n\n7.10 从Props获得派生状态周期\n这个钩子使用极其罕见\nclass Demo extends React.Component { state = {count: 0} // 初始化渲染,组件更新后调用 render() { return ( <div>test</div> ) } // 必须是静态方法,且要返回state对象或者null // 如果state的值完全取决于props,可以使用该钩子,但是也可以使用构造函数替代,所以该钩子应当尽量避免使用 static getDerivedStateFromProps(props, state) { return null // 如果返回类似于state对象的值,会覆盖原有state对象中的属性 // return {count: 100} }}\n7.11 更新前获得快照周期\nclass Demo extends React.Component { state = {count: 0} // 初始化渲染,组件更新后调用 render() { return ( <div>test</div> ) } // 返回的值(null或者快照值可以是数字、字符串,数组、对象)会作为参数传递给componentDidUpdate第三个参数 getSnapshotBeforeUpdate(preProps, preState) { return null } componentDidUpdate(preProps, preState, snapshot) { }}\n8 diffing算法\n\n虚拟DOM中key的作用:\n\n简单的说: key是虚拟DOM对象的标识,\n在更新显示时key起着极其重要的作用。\n详细的说:\n当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,\n随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:\n\n旧虚拟DOM中找到了与新虚拟DOM相同的key:\n\n若虚拟DOM中内容没变, 直接使用之前的真实DOM\n若虚拟DOM中内容变了,\n则生成新的真实DOM,随后替换掉页面中之前的真实DOM\n\n旧虚拟DOM中未找到与新虚拟DOM相同的key根据数据创建新的真实DOM,随后渲染到到页面\n\n\n用index作为key可能会引发的问题: 1.\n若对数据进行:逆序添加、逆序删除等破坏顺序操作:\n会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。 2.\n如果结构中还包含输入类的DOM: 会产生错误DOM更新 ==> 界面有问题。 3.\n注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,\n仅用于渲染列表用于展示,使用index作为key是没有问题的。\n开发中如何选择key?:\n\n最好使用每条数据的唯一标识作为key,\n比如id、手机号、身份证号、学号等唯一值。\n如果确定只是简单的展示数据,用index也是可以的。\n\n\n9 脚手架\n9.1 创建项目并启动\n\n全局安装npm install -g create-react-app\n在需要创建项目的目录下create-react-app hello-react\n进入hello-react目录\n命令行执行npm run start启动项目即可看到默认页面\n\n9.2 样式的模块化\n样式文件名要以xxx.module.css结尾\n.color { background-color: aqua;}\n在jsx组件里引入\nimport hello from \"./Hello.module.css\"function Hello() { return <div className={hello.color}>Hello React! test</div>}export default Hello;\n样式模块化主要是为了防止不同组件中样式名相同导致的样式覆盖问题。或者使用less嵌套编写样式。\n9.3 vscode插件\nvscode插件,提供一些代码片段,提高编码速度\n\n9.4 组件化编码流程\n\n拆分组件:拆分界面,抽取组件\n实现静态组件:使用组件实现静态页面效果\n实现动态组件:\n\n动态显示初始化数据\n\n数据类型\n数据名称\n保存在哪个组件\n\n交互(从绑定事件监听开始)\n\n\n10 消息订阅与发布pubsub\n\n安装包\nnpm install pubsub-js\n引入\nimport PubSub from \"pubsub-js\"\n订阅消息\nimport React, { Component } from \"react\"import PubSub from \"pubsub-js\"export default class Subscribe extends Component { state = { info: \"lishan\" } // 接收消息的方法参数为两个分别是消息名(主题名)和接受的实际数据 receiveInfo = (msg, info) => { this.setState({ info }) } // 组件挂载完成后订阅消息 componentDidMount() { // 订阅哪个消息,以及用哪个方法处理该消息的数据,返回toekn表示该订阅的唯一标识符,方便后期取消订阅 this.token = PubSub.subscribe(\"test\", this.receiveInfo) } /* 或者这么写,直接写在订阅里 componentDidMount() { this.token = PubSub.subscribe(\"test\", (msg, info) => { this.setState({ info }) }) } */ // 组件将要卸载时取消订阅 componentWillUnmount() { PubSub.unsubscribe(this.token) } render() { return <div>{this.state.info}</div> }}\n发布消息\nimport PubSub from \"pubsub-js\"import React, { Component } from \"react\"export default class Publish extends Component { sendInfo = () => { // 发送消息,参数为消息名(主题名)和实际的数据 PubSub.publish(\"test\", \"test PubSub\") } render() { return ( <div> <button onClick={this.sendInfo}>发送消息</button> </div> ) }}\n\n11 fetch(待更新)\n12 React-Router 5\n12.1 库安装\nreact-router总共分为三个版本,分别给web\nnative和任意端使用,这里只安装web端,方便使用。\nnpm install react-router-dom\n12.2 简单使用\n❗当前版本为5版本,新版本会有不同\nApp.jsx代码\nimport React, { Component } from \"react\"import { BrowserRouter as Router, Route, Link } from \"react-router-dom\"import Home from \"./components/Home\"import About from \"./components/About\"export default class App extends Component { render() { return ( // 两个组件都需要包在Router标签里 <Router> <div> {/* Link组件控制跳转到哪个链接 */} <Link to=\"/home\">home</Link><br /> <Link to=\"/about\">about</Link><br /> {/* Route组件控制链接对应的组件。当前是5版本的路由,6版本的路由会出现一些变化 */} <Route path=\"/home\" component={Home} /> <Route path=\"/about\" component={About} /> </div> </Router> ) }}\nHome组件代码\nimport React, { Component } from \"react\"export default class Home extends Component { render() { return <button>Home</button> }}\nAbout组件代码\nimport React, { Component } from \"react\";export default class About extends Component { render() { return <button>About</button>; }}\n12.3 路由组件和一般组件\n\n写法不同:\n一般组件:<Demo />\n路由组件:<Route path=\"/demo \" component={Demo } />\n存放位置不同\n一般组件:components\n路由组件:pages\n接收到的props不同\n路由组件会收到以下内容(只列举出常用的):\nhistory: \taction: \"PUSH\"\tblock: ƒ block(prompt)\tcreateHref: ƒ createHref(location)\tgo: ƒ go(n)\tgoBack: ƒ goBack()\tgoForward: ƒ goForward()\tlength: 4\tlisten: ƒ listen(listener)\tpush: ƒ push(path, state)\treplace: ƒ replace(path, state)location: \thash: \"\"\tkey: \"o2ti2g\"\tpathname: \"/home\"\tsearch: \"\"\tstate: undefinedmatch: \tisExact: true\tparams: {}\tpath: \"/home\"\turl: \"/home\"\n\n12.4 NavLink组件\nNavLink可以指定选中某个链接时使用哪个样式\n<NavLink activeClassName=\"class-name\" to=\"/home\">home</NavLink>\n封装NavLink组件\n如果多个NavLink组件共用样式,会造成样式的代码冗余,对此进行封装为MyNavLink组件,代码为:\nimport React, { Component } from \"react\";import { NavLink } from \"react-router-dom\";export default class MyNavLink extends Component { render() { return <NavLink activeClassName=\"\" className=\"\" {...this.props} /> }}\n在其他组件使用时直接和NavLink组件一样使用即可:\n<MyNavLink to=\"/home\" otherParams={1}>Home</MyNavLink>\n❗组件使用props传值时,除了可以自定义标签属性传值如:<Demo a={1} />,这样会将a传递到this.props.a中。也可以将标签体中的内容传递到props中,使用this.props.children属性保存标签体的内容,标签体存储的key这个是固定的名字无法修改。\n自定义封装的MyNavLink组件,在标签体中写入内容会被传到this.props.children中,而children会传递给NavLink中的标签属性中的children,达到<NavLink>test</NavLink>的效果。\n12.5 Switch组件\n进行路由匹配时,会逐个比对匹配,如果有多个路径匹配则会展示多个组件页面,影响效率。此时在外层包裹Switch组件,保证匹配上一个路由后停止匹配。(因为多数情况下一个路径匹配一个组件)\n<Switch> <Route path=\"/home\" component={Home} /> <Route path=\"/about\" component={About} /></Switch>\n12.6 多级路由刷新页面央视丢失\n\npublic/index.html中引入样式时不用./用/\npublic/index.html中引入样式时不用./用%PUBLIC_URL%(常用),%PUBLIC_URL%脚手架提供的,表示绝对路径\n\n12.7 路由模糊匹配与严格匹配\nReact默认开始模糊匹配\n<Link to=\"/home\">home</Link><br /><Link to=\"/about/a/b\">about</Link><br />{/* Route组件控制链接对应的组件。当前是5版本的路由,6版本的路由会出现一些变化 */}<Route path=\"/home\" component={Home} /><Route path=\"/about\" component={About} />\n如上所示的代码,默认模糊匹配时,链接设置的路径前缀包含路由的路径,则可以匹配上。\n<Route exact path=\"/about\" component={About} />\n如果开启严格匹配则不能匹配上。\n❗默认情况下不做修改,只用模糊匹配即可。如果出现使用模糊匹配导致页面有问题,再开启严格匹配。\n12.8 Redirect重定向\n<Route path=\"/home\" component={Home} /><Route path=\"/about\" component={About} />// 一般把Redirect组件写在所有路由组件最下方<Redirect to=\"/home\" />\n12.9 嵌套路由\n一级路由\n<Switch> <Route path=\"/home\" component={Home} /> <Route path=\"/about\" component={About} /></Switch>\n二级路由\n// 注册子路由要写上父路由的path值<MyNavLink to=\"/home/news\">News</MyNavLink><MyNavLink to=\"/home/message\">Message</MyNavLink><Switch> <Route path=\"/home/news\" component={News} /> <Route path=\"/home/message\" component={Message} /></Switch>\n点击/home,路由匹配时,会按照注册顺序进行匹配,首先展示一级路由,路由按顺序匹配,匹配到了/home路径展示Home组件,该组件会有News Message两个路由组件。当点击/home/news时,根据路由的模糊匹配,首先会匹配到一级路由/home,渲染该组件到页面中,会把子组件也渲染,就会注册二级路由,进而进行路由匹配,会匹配到/home/news,展示News组件。\n此时如果开启严格模式,则会导致不能使用二级路由,因为一级路由没有匹配上,二级路由无法注册,也无法匹配。\n12.10 路由传参\n\n传递params参数\n<MyNavLink to=\"/home/news\">News</MyNavLink>// 通过在链接最后拼接的方式将参数携带到链接里<MyNavLink to={`/home/message/${infoId}`}>Message</MyNavLink><Switch> <Route path=\"/home/news\" component={News} /> {/* 接收参数时使用 :infoId 的形式占位,表示接收参数并指定参数的名字 */} <Route path=\"/home/message/:infoId\" component={Message} /></Switch>\n接收到的参数在组件里的this.props.match.params对象里。\n传递search参数\n<MyNavLink to=\"/home/news\">News</MyNavLink>// 通过?在链接最后拼接key=value&key=value的形式传参<MyNavLink to={`/home/message?infoId=${infoId}`}>Message</MyNavLink><Switch> <Route path=\"/home/news\" component={News} /> {/* 注册路由无需改变 */} <Route path=\"/home/message\" component={Message} /></Switch>\n接收到的参数保存在:this.props.location.search字符串里,是一个urlencoded编码的字符串,需要手动转成字典对象,可以使用第三方库querystring简称qs。\n传递state参数\n<MyNavLink to=\"/home/news\">News</MyNavLink>// 把to属性的值写成一个对象<MyNavLink to={{pathname: \"/home/message\", state: {infoId: 1}}}>Message</MyNavLink><Switch> <Route path=\"/home/news\" component={News} /> <Route path=\"/home/message\" component={Message} /></Switch>\n接收到的参数保存在this.props.location.state对象中。此种方式传递的参数不会显示在地址栏中,隐私性较高。刷新也会保留住参数。如果清除缓存,则不会保留住。\n\n12.11 push和replace\n默认使用push模式,切换路由会向历史栈中压栈,如果使用replace则会替换当前页面,不会压栈。\n12.12 编程式路由导航\n编程式代替Link组件。\n// 传递paramsthis.props.history.push(`/home/message/${id}`)this.props.history.replace(`/home/message/${id}`)// 传递searchthis.props.history.push(`/home/message?id=1`)this.props.history.replace(`/home/message?id=1`)// 传递statethis.props.history.push(\"/home/message\", {id: 1})this.props.history.replace(\"/home/message\", {id: 1})\n12.13 withRouter\n主要用于一般组件中使用路由组件特有的方法如:push replace等。\nimport {withRouter} from \"react-router-dom\"class Home extends Component { }// 到处前使用withRouter函数包装一下,即可使Home组件拥有路由组件的参数,即可使用push等方法export default withRouter(Home)\n13 redux(待更新)\n14 扩展\n14.1 setState\nsetState更新状态的2种写法\n\t(1). setState(stateChange, [callback])------对象式的setState 1.stateChange为状态改变对象(该对象可以体现出状态的更改) 2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用\t\t\t\t\t\t(2). setState(updater, [callback])------函数式的setState 1.updater为返回stateChange对象的函数。 2.updater可以接收到state和props。 4.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。总结:\t\t1.对象式的setState是函数式的setState的简写方式(语法糖)\t\t2.使用原则:\t\t\t\t(1).如果新状态不依赖于原状态 ===> 使用对象方式\t\t\t\t(2).如果新状态依赖于原状态 ===> 使用函数方式\t\t\t\t(3).如果需要在setState()执行后获取最新的状态数据, \t\t\t\t\t要在第二个callback函数中读取\n14.2 lazyLoad\n路由组件的lazyLoad\n//1.通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包const Login = lazy(()=>import('@/pages/Login'))//2.通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面<Suspense fallback={<h1>loading.....</h1>}> <Switch> <Route path=\"/xxx\" component={Xxxx}/> <Redirect to=\"/login\"/> </Switch> </Suspense>\n14.3 Hooks\n\nReact Hook/Hooks是什么?\n\n(1). Hook是React 16.8.0版本增加的新特性/新语法(2). 可以让你在函数组件中使用 state 以及其他的 React 特性\n\n三个常用的Hook\n\n(1). State Hook: React.useState()(2). Effect Hook: React.useEffect()(3). Ref Hook: React.useRef()\n\nState Hook\n\n(1). State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作(2). 语法: const [xxx, setXxx] = React.useState(initValue) (3). useState()说明: 参数: 第一次初始化指定的值在内部作缓存 返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数(4). setXxx()2种写法: setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值 setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值\n\nEffect Hook\n\n(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)(2). React中的副作用操作: 发ajax请求数据获取 设置订阅 / 启动定时器 手动更改真实DOM(3). 语法和说明: useEffect(() => { // 在此可以执行任何带副作用操作 return () => { // 在组件卸载前执行 // 在此做一些收尾工作, 比如清除定时器/取消订阅等 } }, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行 (4). 可以把 useEffect Hook 看做如下三个函数的组合 componentDidMount() componentDidUpdate() \tcomponentWillUnmount() \n\nRef Hook\n\n(1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据(2). 语法: const refContainer = useRef()(3). 作用:保存标签对象,功能与React.createRef()一样\n14.4 Fragment\n使用\n<Fragment>\n</Fragment>\n或者写<></>\n作用\n\n可以不用必须有一个真实的DOM根标签了\n\n\n14.5 Context\n理解\n\n一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信\n\n使用\n1) 创建Context容器对象:\tconst XxxContext = React.createContext() \t2) 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:\t<xxxContext.Provider value={数据}>\t\t子组件 </xxxContext.Provider> 3) 后代组件读取数据:\t//第一种方式:仅适用于类组件 \t static contextType = xxxContext // 声明接收context\t this.context // 读取context中的value数据\t \t//第二种方式: 函数组件与类组件都可以\t <xxxContext.Consumer>\t {\t value => ( // value就是context中的value数据\t 要显示的内容\t )\t }\t </xxxContext.Consumer>\n注意\n在应用开发中一般不用context, 一般都用它的封装react插件\n\n14.6 组件优化\nComponent的2个问题\n\n\n只要执行setState(),即使不改变状态数据,\n组件也会重新render()\n只当前组件重新render(), 就会自动重新render子组件 ==>\n效率低\n\n\n效率高的做法\n\n只有当组件的state或props数据发生改变时才重新render()\n\n原因\n\nComponent中的shouldComponentUpdate()总是返回true\n\n解决\n办法1: \n 重写shouldComponentUpdate()方法\n 比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false\n办法2: \n 使用PureComponent\n PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true\n 注意: \n 只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false \n 不要直接修改state数据, 而是要产生新数据\n项目中一般使用PureComponent来优化\n\n14.7 render props\n如何向组件内部动态传入带内容的结构(标签)?\nVue中: \n 使用slot技术, 也就是通过组件标签体传入结构 <AA><BB/></AA>\nReact中:\n 使用children props: 通过组件标签体传入结构\n 使用render props: 通过组件标签属性传入结构, 一般用render函数属性\nchildren props\n<A>\n <B>xxxx</B>\n</A>\n{this.props.children}\n问题: 如果B组件需要A组件内的数据, ==> 做不到 \nrender props\n<A render={(data) => <C data={data}></C>}></A>\nA组件: {this.props.render(内部state数据)}\nC组件: 读取A组件传入的数据显示 {this.props.data} \n14.8 错误边界\n理解:\n错误边界:用来捕获后代组件错误,渲染出备用页面\n特点:\n只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误\n使用方式:\ngetDerivedStateFromError配合componentDidCatch\n// 生命周期函数,一旦后台组件报错,就会触发static getDerivedStateFromError(error) { console.log(error); // 在render之前触发 // 返回新的state return { hasError: true, };}componentDidCatch(error, info) { // 统计页面的错误。发送请求发送到后台去 console.log(error, info);}\n14.9 组件通信方式总结\n方式:\n props:\n (1).children props\n (2).render props\n 消息订阅-发布:\n pubs-sub、event等等\n 集中式管理:\n redux、dva等等\n conText:\n 生产者-消费者模式\n组件间的关系\n 父子组件:props\n 兄弟组件(非嵌套组件):消息订阅-发布、集中式管理\n 祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(用的少)\n15 React-Router 6\n15.1 概述\n\nReact Router 以三个不同的包发布到 npm 上,它们分别为:\n\nreact-router: 路由的核心库,提供了很多的:组件、钩子。\nreact-router-dom:</strong >\n包含react-router所有内容,并添加一些专门用于\nDOM 的组件,例如 <BrowserRouter>等 。\nreact-router-native:\n包括react-router所有内容,并添加一些专门用于ReactNative的API,例如:<NativeRouter>等。\n\n与React Router 5.x 版本相比,改变了什么?\n\n内置组件的变化:移除<Switch/> ,新增\n<Routes/>等。\n语法的变化:component={About} 变为\nelement={<About/>}等。\n新增多个hook:useParams、useNavigate、useMatch等。\n官方明确推荐函数式组件了!!!\n......\n\n\n15.2 Component\n\n<BrowserRouter>\n\n说明:<BrowserRouter>用于包裹整个应用。\n示例代码:\n\nimport React from \"react\";import ReactDOM from \"react-dom\";import { BrowserRouter } from \"react-router-dom\";ReactDOM.render( <BrowserRouter> {/* 整体结构(通常为App组件) */} </BrowserRouter>,root);\n<HashRouter>\n\n说明:作用与<BrowserRouter>一样,但<HashRouter>修改的是地址栏的hash值。\n备注:6.x版本中<HashRouter>、<BrowserRouter>\n的用法与 5.x 相同。\n\n<Routes/> 与 <Route/>\n\nv6版本中移出了先前的<Switch>,引入了新的替代者:<Routes>。\n<Routes> 和\n<Route>要配合使用,且必须要用<Routes>包裹<Route>。\n<Route> 相当于一个 if 语句,如果其路径与当前 URL\n匹配,则呈现其对应的组件。\n<Route caseSensitive>\n属性用于指定:匹配时是否区分大小写(默认为 false)。\n当URL发生变化时,<Routes>都会查看其所有子<Route>\n元素以找到最佳匹配并呈现组件 。\n<Route>\n也可以嵌套使用,且可配合useRoutes()配置 “路由表”\n,但需要通过 <Outlet> 组件来渲染其子路由。\n示例代码:\n\n<Routes> {/* path属性用于定义路径,element属性用于定义当前路径所对应的组件 */} <Route path=\"/login\" element={<Login />}></Route> {/* 用于定义嵌套路由,home是一级路由,对应的路径/home */} <Route path=\"home\" element={<Home />}> {/* test1 和 test2 是二级路由,对应的路径是/home/test1 或 /home/test2 */} <Route path=\"test1\" element={<Test/>}></Route> <Route path=\"test2\" element={<Test2/>}></Route>\t</Route>\t {/* Route也可以不写element属性, 这时就是用于展示嵌套的路由 .所对应的路径是/users/xxx */} <Route path=\"users\"> <Route path=\"xxx\" element={<Demo />} /> </Route></Routes>\n<Link>\n\n作用: 修改URL,且不发送网络请求(路由链接)。\n注意:\n外侧需要用<BrowserRouter>或<HashRouter>包裹。\n示例代码:\n\nimport { Link } from \"react-router-dom\";function Test() { return ( <div> \t<Link to=\"/路径\">按钮</Link> </div> );}\n<NavLink>\n\n作用:\n与<Link>组件类似,且可实现导航的“高亮”效果。\n示例代码:\n\n// 注意: NavLink默认类名是active,下面是指定自定义的class//自定义样式,如果NavLink过多,可以将箭头函数单独定义,减少代码量<NavLink to=\"login\" className={({ isActive }) => { console.log('home', isActive) return isActive ? 'base one' : 'base' }}>login</NavLink>/*\t默认情况下,当Home的子组件匹配成功,Home的导航也会高亮,\t当NavLink上添加了end属性后,若Home的子组件匹配成功,则Home的导航没有高亮效果。*/<NavLink to=\"home\" end >home</NavLink>\n<Navigate>\n可以把该组件理解为Redirect重定向\n\n作用:只要<Navigate>组件被渲染,就会修改路径,切换视图。\nreplace属性用于控制跳转模式(push 或\nreplace,默认是push)。\n示例代码:\n\nimport React,{useState} from 'react'import {Navigate} from 'react-router-dom'export default function Home() {\tconst [sum,setSum] = useState(1)\treturn (\t\t<div>\t\t\t<h3>我是Home的内容</h3>\t\t\t{/* 根据sum的值决定是否切换视图 */}\t\t\t{sum === 1 ? <h4>sum的值为{sum}</h4> : <Navigate to=\"/about\" replace={true}/>}\t\t\t<button onClick={()=>setSum(2)}>点我将sum变为2</button>\t\t</div>\t)}\n<Outlet>\n\n当<Route>产生嵌套时,渲染其对应的后续子路由。非嵌套时不用这个组件\n示例代码:\n\n//根据路由表生成对应的路由规则const element = useRoutes([ { path:'/about', element:<About/> }, { path:'/home', element:<Home/>, children:[ { path:'news', element:<News/> }, { path:'message', element:<Message/>, } ] }])//Home.jsimport React from 'react'import {NavLink,Outlet} from 'react-router-dom'export default function Home() {\treturn (\t\t<div>\t\t\t<h2>Home组件内容</h2>\t\t\t<div>\t\t\t\t<ul className=\"nav nav-tabs\">\t\t\t\t\t<li> {/* to可以直接写子路由的path,注意不要加/,如果加了则表示从头开始匹配,不加则是在当前路由下匹配 */}\t\t\t\t\t\t<NavLink className=\"list-group-item\" to=\"news\">News</NavLink>\t\t\t\t\t</li>\t\t\t\t\t<li>\t\t\t\t\t\t<NavLink className=\"list-group-item\" to=\"message\">Message</NavLink>\t\t\t\t\t</li>\t\t\t\t</ul>\t\t\t\t{/* 指定路由组件呈现的位置 */}\t\t\t\t<Outlet />\t\t\t</div>\t\t</div>\t)}\n\n15.3 Hooks\n\nuseRoutes()\n\n作用:根据路由表,动态创建<Routes>和<Route>。\n示例代码:\n\n//路由表配置:src/routes/index.jsimport About from '../pages/About'import Home from '../pages/Home'import {Navigate} from 'react-router-dom'export default [ { path:'/about', element:<About/> }, { path:'/home', element:<Home/> }, { path:'/', element:<Navigate to=\"/about\"/> }]//App.jsximport React from 'react'import {NavLink,useRoutes} from 'react-router-dom'import routes from './routes'export default function App() { //根据路由表生成对应的路由规则 const element = useRoutes(routes) return ( <div> ...... {/* 注册路由 */} {element} ...... </div> )}\nuseNavigate()\n\n作用:返回一个函数用来实现编程式导航。\n示例代码:\n\nimport React from 'react'import {useNavigate} from 'react-router-dom'export default function Demo() { const navigate = useNavigate() const handle = () => { //第一种使用方式:指定具体的路径 navigate('/login', { replace: false, state: {a:1, b:2} }) //第二种使用方式:传入数值进行前进或后退,类似于5.x中的 history.go()方法 navigate(-1) // 后退 // navigate(1) // 前进 } return ( <div> <button onClick={handle}>按钮</button> </div> )}\nuseParams()\n\n作用:返回当前匹配路由的params参数,类似于5.x中的match.params。\n示例代码:\n\nimport React from 'react';import { Routes, Route, useParams } from 'react-router-dom';import User from './pages/User.jsx'function ProfilePage() { // 获取URL中携带过来的params参数 let { id } = useParams();}function App() { return ( <Routes> <Route path=\"users/:id\" element={<User />}/> </Routes> );}\nuseSearchParams()\n\n作用:用于读取和修改当前位置的 URL 中的查询字符串。\n返回一个包含两个值的数组,内容分别为:当前的seaech参数、更新search的函数。\n示例代码:\n\nimport React from 'react'import {useSearchParams} from 'react-router-dom'export default function Detail() {\tconst [search,setSearch] = useSearchParams()\tconst id = search.get('id')\tconst title = search.get('title')\tconst content = search.get('content')\treturn (\t\t<ul>\t\t\t<li>\t\t\t\t<button onClick={()=>setSearch('id=008&title=哈哈&content=嘻嘻')}>点我更新一下收到的search参数</button>\t\t\t</li>\t\t\t<li>消息编号:{id}</li>\t\t\t<li>消息标题:{title}</li>\t\t\t<li>消息内容:{content}</li>\t\t</ul>\t)}\nuseLocation()\n\n作用:获取当前 location\n信息,对标5.x中的路由组件的location属性。\n示例代码:\n\n// 传递state参数// <Link to=\"/home\" state={{id: 1}} >主页</Link>// 接收使用useLocation()获取location对象,该对象的state存储的是参数import React from 'react'import {useLocation} from 'react-router-dom'export default function Detail() {\tconst x = useLocation()\tconsole.log('@',x) // x就是location对象: \t/*\t\t{ hash: \"\", key: \"ah9nv6sz\", pathname: \"/login\", search: \"?name=zs&age=18\", state: {a: 1, b: 2} }\t*/\treturn (\t\t<ul>\t\t\t<li>消息编号:{id}</li>\t\t\t<li>消息标题:{title}</li>\t\t\t<li>消息内容:{content}</li>\t\t</ul>\t)}\nuseMatch()\n\n作用:返回当前匹配信息,对标5.x中的路由组件的match属性。\n示例代码:\n\n<Route path=\"/login/:page/:pageSize\" element={<Login />}/><NavLink to=\"/login/1/10\">登录</NavLink>export default function Login() { const match = useMatch('/login/:x/:y') console.log(match) //输出match对象 //match对象内容如下: /* \t{ params: {x: '1', y: '10'} pathname: \"/LoGin/1/10\" pathnameBase: \"/LoGin/1/10\" pattern: { \tpath: '/login/:x/:y', \tcaseSensitive: false, \tend: false } } */ return ( \t<div> <h1>Login</h1> </div> )}\nuseInRouterContext()\n\n 作用:如果组件在 <Router> 的上下文中呈现,则\nuseInRouterContext 钩子返回 true,否则返回 false。\n\nuseNavigationType()\n\n作用:返回当前的导航类型(用户是如何来到当前页面的)。\n返回值:POP、PUSH、REPLACE。\n备注:POP是指在浏览器中直接打开了这个路由组件(刷新页面)。\n\nuseOutlet()\n\n作用:用来呈现当前组件中渲染的嵌套路由。\n示例代码:\n\nconst result = useOutlet()console.log(result)// 如果嵌套路由没有挂载,则result为null// 如果嵌套路由已经挂载,则展示嵌套的路由对象\nuseResolvedPath()\n作用:给定一个 URL值,解析其中的:path、search、hash值。\n\n","categories":["技术"],"tags":["React"]},{"title":"TypeScript基础学习","url":"/2023/12/01/%E5%89%8D%E7%AB%AF/TypeScript%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0/","content":"1 TypeScript开发环境搭建\n\n下载安装Node.js\n官网,下载长期稳定版(LTS)\n\n\n\n全局安装typescript\nnpm install -g typescript\n创建ts文件\n使用tsc对ts文件进行编译\ntsc xxx.ts\n\n2 类型\n\n类型声明\n\n类型声明是TS非常重要的一个特点\n通过类型声明可以指定TS中变量(参数、形参)的类型\n指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错\n简而言之,类型声明给变量设置了类型,使得变量只能存储某种类型的值\n语法:\nlet 变量: 类型;let 变量: 类型 = 值;function fn(参数: 类型, 参数: 类型): 类型{ ...}\n\n自动类型判断\n\nTS拥有自动的类型判断机制\n当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型\n所以如果你的变量的声明和赋值时同时进行的,可以省略掉类型声明\n\n类型:\n\n\n\n\n类型\n例子\n描述\n\n\n\n\nnumber\n1, -33, 2.5\n任意数字\n\n\nstring\n'hi', \"hi\", hi\n任意字符串\n\n\nboolean\ntrue、false\n布尔值true或false\n\n\n字面量\n其本身\n限制变量的值就是该字面量的值\n\n\nany\n*\n任意类型\n\n\nunknown\n*\n类型安全的any\n\n\nvoid\n空值(undefined)\n没有值(或undefined)\n\n\nnever\n没有值\n不能是任何值\n\n\nobject\n{name:'孙悟空'}\n任意的JS对象\n\n\narray\n[1,2,3]\n任意JS数组\n\n\ntuple\n[4,5]\n元素,TS新增类型,固定长度数组\n\n\nenum\nenum{A, B}\n枚举,TS中新增类型\n\n\n\n\nnumber\nlet decimal: number = 6;let hex: number = 0xf00d;let binary: number = 0b1010;let octal: number = 0o744;let big: bigint = 100n;\nboolean\nlet isDone: boolean = false;\nstring\nlet color: string = \"blue\";color = 'red';let fullName: string = `Bob Bobbington`;let age: number = 37;let sentence: string = `Hello, my name is ${fullName}.I'll be ${age + 1} years old next month.`;\n字面量\n\n也可以使用字面量去指定变量的类型,通过字面量可以确定变量的取值范围\nlet color: 'red' | 'blue' | 'black';let num: 1 | 2 | 3 | 4 | 5;\n\nany\nlet d: any = 4;d = 'hello';d = true;let a: boolean = true;// 不会提示错误a = d;\nunknown\nlet notSure: unknown = 4;notSure = 'hello';let a: boolean = true;// 提示错误a = notSure;// 如果确定类型相同想赋值,可以使用类型断言或者先进行类型检查// if (typeof notSure === \"boolean\")\nvoid\nlet unusable: void = undefined;\nnever\nfunction error(message: string): never { throw new Error(message);}\nobject(没啥用)\nlet obj: object = {};\narray\nlet list: number[] = [1, 2, 3];let list: Array<number> = [1, 2, 3];\ntuple\nlet x: [string, number];x = [\"hello\", 10]; \nenum\nenum Color { Red, Green, Blue,}let c: Color = Color.Green;enum Color { Red = 1, Green, Blue,}let c: Color = Color.Green;enum Color { Red = 1, Green = 2, Blue = 4,}let c: Color = Color.Green;\n类型断言\n\n有些情况下,变量的类型对于我们来说是很明确,但是TS编译器却并不清楚,此时,可以通过类型断言来告诉编译器变量的类型,断言有两种形式:\n\n第一种\nlet someValue: unknown = \"this is a string\";let strLength: number = (someValue as string).length;\n第二种\nlet someValue: unknown = \"this is a string\";let strLength: number = (<string>someValue).length;\n\n\ntype\n// 定义a的对象结构类型,其中name属性必须包含,age属性可选let a: {name: string, age?: number}// 定义b的对象结构类型,name属性必须包含,同时可以添加其他的属性,属性名为string类型,值为number类型let b: {name: string, [propName: string]: number}// 定义函数的类型,参数类型、返回值类型let dFn: (a: number, b: number) => numbertype testType = { name: string, [prop: string]: number}\n\n3 编译选项\n\n自动编译文件\n\n编译文件时,使用 -w\n指令后,TS编译器会自动监视文件的变化,并在文件发生变化时对文件进行重新编译。\n示例:\ntsc xxx.ts -w\n\n自动编译整个项目\n\n如果直接使用tsc指令,则可以自动将当前项目下的所有ts文件编译为js文件。\n但是能直接使用tsc命令的前提时,要先在项目根目录下创建一个ts的配置文件\ntsconfig.json\ntsconfig.json是一个JSON文件,添加配置文件后,只需只需 tsc\n命令即可完成对整个项目的编译\n配置选项:\n\ninclude\n\n定义希望被编译文件所在的目录\n默认值:[\"**/*\"]\n示例:\n\"include\":[\"src/**/*\", \"tests/**/*\"]\n\n上述示例中,所有src目录和tests目录下的文件都会被编译\n\n\nexclude\n\n定义需要排除在外的目录\n默认值:[\"node_modules\", \"bower_components\",\n\"jspm_packages\"]\n示例:\n\"exclude\": [\"./src/hello/**/*\"]\n\n上述示例中,src下hello目录下的文件都不会被编译\n\n\nextends\n\n定义被继承的配置文件\n示例:\n\"extends\": \"./configs/base\"\n\n上述示例中,当前配置文件中会自动包含config目录下base.json中的所有配置信息\n\n\nfiles\n\n指定被编译文件的列表,只有需要编译的文件少时才会用到\n示例:\n\"files\": [ \"core.ts\", \"sys.ts\", \"types.ts\", \"scanner.ts\", \"parser.ts\", \"utilities.ts\", \"binder.ts\", \"checker.ts\", \"tsc.ts\" ]\n\n列表中的文件都会被TS编译器所编译\n\ncompilerOptions\n\n编译选项是配置文件中非常重要也比较复杂的配置选项\n在compilerOptions中包含多个子选项,用来完成对编译的配置\n\n项目选项\n\ntarget\n\n设置ts代码编译的目标版本\n可选值:\n\nES3(默认)、ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext\n\n示例:\n\"compilerOptions\": { \"target\": \"ES6\"}\n\n如上设置,我们所编写的ts代码将会被编译为ES6版本的js代码\n\n\nlib\n\n指定代码运行时所包含的库(宿主环境)\n可选值:\n\nES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext、DOM、WebWorker、ScriptHost\n......\n\n示例:\n\"compilerOptions\": { \"target\": \"ES6\", \"lib\": [\"ES6\", \"DOM\"], \"outDir\": \"dist\", \"outFile\": \"dist/aa.js\"}\n\nmodule\n\n设置编译后代码使用的模块化系统\n可选值:\n\nCommonJS、UMD、AMD、System、ES2020、ESNext、None\n\n示例:\n\"compilerOptions\": { \"module\": \"CommonJS\"}\n\noutDir\n\n编译后文件的所在目录\n默认情况下,编译后的js文件会和ts文件位于相同的目录,设置outDir后可以改变编译后文件的位置\n示例:\n\"compilerOptions\": { \"outDir\": \"dist\"}\n\n设置后编译后的js文件将会生成到dist目录\n\n\noutFile\n\n将所有的文件编译为一个js文件\n默认会将所有的编写在全局作用域中的代码合并为一个js文件,如果module制定了None、System或AMD则会将模块一起合并到文件之中\n示例:\n\"compilerOptions\": { \"outFile\": \"dist/app.js\"}\n\nrootDir\n\n指定代码的根目录,默认情况下编译后文件的目录结构会以最长的公共目录为根目录,通过rootDir可以手动指定根目录\n示例:\n\"compilerOptions\": { \"rootDir\": \"./src\"}\n\nallowJs\n\n是否对js文件编译\n\ncheckJs\n\n是否对js文件进行检查\n示例:\n\"compilerOptions\": { \"allowJs\": true, \"checkJs\": true}\n\nremoveComments\n\n是否删除注释\n默认值:false\n\nnoEmit\n\n不对代码进行编译\n默认值:false\n\nsourceMap\n\n是否生成sourceMap\n默认值:false\n\n\n严格检查\n\nstrict\n\n启用所有的严格检查,默认值为true,设置后相当于开启了所有的严格检查\n\nalwaysStrict\n\n总是以严格模式对代码进行编译\n\nnoImplicitAny\n\n禁止隐式的any类型\n\nnoImplicitThis\n\n禁止类型不明确的this\n\nstrictBindCallApply\n\n严格检查bind、call和apply的参数列表\n\nstrictFunctionTypes\n\n严格检查函数的类型\n\nstrictNullChecks\n\n严格的空值检查\n\nstrictPropertyInitialization\n\n严格检查属性是否初始化\n\n\n额外检查\n\nnoFallthroughCasesInSwitch\n\n检查switch语句包含正确的break\n\nnoImplicitReturns\n\n检查函数没有隐式的返回值\n\nnoUnusedLocals\n\n检查未使用的局部变量\n\nnoUnusedParameters\n\n检查未使用的参数\n\n\n高级\n\nallowUnreachableCode\n\n检查不可达代码\n可选值:\n\ntrue,忽略不可达代码\nfalse,不可达代码将引起错误\n\n\nnoEmitOnError\n\n有错误的情况下不进行编译\n默认值:false\n\n\n\n\n\n\n\n\n4 webpack\n\n通常情况下,实际开发中我们都需要使用构建工具对代码进行打包,TS同样也可以结合构建工具一起使用,下边以webpack为例介绍一下如何结合构建工具使用TS。\n步骤:\n\n初始化项目\n\n进入项目根目录,执行命令 npm init -y\n\n主要作用:创建package.json文件\n\n\n下载构建工具\n\nnpm i -D webpack webpack-cli webpack-dev-server typescript ts-loader clean-webpack-plugin\n\n共安装了7个包\n\nwebpack\n\n构建工具webpack\n\nwebpack-cli\n\nwebpack的命令行工具\n\nwebpack-dev-server\n\nwebpack的开发服务器\n\ntypescript\n\nts编译器\n\nts-loader\n\nts加载器,用于在webpack中编译ts文件\n\nhtml-webpack-plugin\n\nwebpack中html插件,用来自动创建html文件\n\nclean-webpack-plugin\n\nwebpack中的清除插件,每次构建都会先清除目录\n\n\n\n\n根目录下创建webpack的配置文件webpack.config.js\nconst path = require(\"path\");const HtmlWebpackPlugin = require(\"html-webpack-plugin\");const { CleanWebpackPlugin } = require(\"clean-webpack-plugin\");module.exports = { optimization:{ minimize: false // 关闭代码压缩,可选 }, entry: \"./src/index.ts\", devtool: \"inline-source-map\", devServer: { contentBase: './dist' }, output: { path: path.resolve(__dirname, \"dist\"), filename: \"bundle.js\", environment: { arrowFunction: false // 关闭webpack的箭头函数,可选 } }, resolve: { extensions: [\".ts\", \".js\"] }, module: { rules: [ { test: /\\.ts$/, use: { loader: \"ts-loader\" }, exclude: /node_modules/ } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title:'TS测试' }), ]}\n根目录下创建tsconfig.json,配置可以根据自己需要\n{ \"compilerOptions\": { \"target\": \"ES2015\", \"module\": \"ES2015\", \"strict\": true }}\n修改package.json添加如下配置\n{ ...略... \"scripts\": { \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\", \"build\": \"webpack\", \"start\": \"webpack serve --open chrome.exe\" }, ...略...}\n在src下创建ts文件,并在并命令行执行npm run build对代码进行编译,或者执行npm start来启动开发服务器\n\n\n5 Babel\n\n经过一系列的配置,使得TS和webpack已经结合到了一起,除了webpack,开发中还经常需要结合babel来对代码进行转换以使其可以兼容到更多的浏览器,在上述步骤的基础上,通过以下步骤再将babel引入到项目中。\n\n安装依赖包:\n\nnpm i -D @babel/core @babel/preset-env babel-loader core-js\n共安装了4个包,分别是:\n\n@babel/core\n\nbabel的核心工具\n\n@babel/preset-env\n\nbabel的预定义环境\n\n@babel-loader\n\nbabel在webpack中的加载器\n\ncore-js\n\ncore-js用来使老版本的浏览器支持新版ES语法\n\n\n\n修改webpack.config.js配置文件\n...略...module: { rules: [ { test: /\\.ts$/, use: [ { loader: \"babel-loader\", options:{ presets: [ [ \"@babel/preset-env\", { \"targets\":{ \"chrome\": \"58\", \"ie\": \"11\" }, \"corejs\":\"3\", \"useBuiltIns\": \"usage\" } ] ] } }, { loader: \"ts-loader\", } ], exclude: /node_modules/ } ]}...略...\n\n如此一来,使用ts编译后的文件将会再次被babel处理,使得代码可以在大部分浏览器中直接使用,可以在配置选项的targets中指定要兼容的浏览器版本。\n\n\n\n6 面向对象\n6.1 类(class)\n要想面向对象,操作对象,首先便要拥有对象,那么下一个问题就是如何创建对象。要创建对象,必须要先定义类,所谓的类可以理解为对象的模型,程序中可以根据类创建指定类型的对象,举例来说:可以通过Person类来创建人的对象,通过Dog类创建狗的对象,通过Car类来创建汽车的对象,不同的类可以用来创建不同的对象。\n\n定义类:\nclass 类名 {\t属性名: 类型;\t\tconstructor(参数: 类型){\t\tthis.属性名 = 参数;\t}\t\t方法名(){\t\t....\t}}\n示例:\nclass Person{ name: string; age: number; constructor(name: string, age: number){ this.name = name; this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); }}\n使用类:\nconst p = new Person('孙悟空', 18);p.sayHello();\n\n6.2 面向对象的特点\n\n封装\n\n对象实质上就是属性和方法的容器,它的主要作用就是存储属性和方法,这就是所谓的封装\n默认情况下,对象的属性是可以任意的修改的,为了确保数据的安全性,在TS中可以对属性的权限进行设置\n只读属性(readonly):\n\n如果在声明属性时添加一个readonly,则属性便成了只读属性无法修改\n\nTS中属性具有三种修饰符:\n\npublic(默认值),可以在类、子类和对象中修改\nprotected ,可以在类、子类中修改\nprivate ,可以在类中修改\n\n示例:\n\npublic\nclass Person{ public name: string; // 写或什么都不写都是public public age: number; constructor(name: string, age: number){ this.name = name; // 可以在类中修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); }}class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中可以修改 }}const p = new Person('孙悟空', 18);p.name = '猪八戒';// 可以通过对象修改\nprotected\nclass Person{ protected name: string; protected age: number; constructor(name: string, age: number){ this.name = name; // 可以修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); }}class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中可以修改 }}const p = new Person('孙悟空', 18);p.name = '猪八戒';// 不能修改\nprivate\nclass Person{ private name: string; private age: number; constructor(name: string, age: number){ this.name = name; // 可以修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); }}class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中不能修改 }}const p = new Person('孙悟空', 18);p.name = '猪八戒';// 不能修改\n\n属性存取器\n\n对于一些不希望被任意修改的属性,可以将其设置为private\n直接将其设置为private将导致无法再通过对象修改其中的属性\n我们可以在类中定义一组读取、设置属性的方法,这种对属性读取或设置的属性被称为属性的存取器\n读取属性的方法叫做setter方法,设置属性的方法叫做getter方法\n示例:\nclass Person{ private _name: string; constructor(name: string){ this._name = name; } get name(){ return this._name; } set name(name: string){ this._name = name; }}const p1 = new Person('孙悟空');console.log(p1.name); // 通过getter读取name属性p1.name = '猪八戒'; // 通过setter修改name属性\n\n静态属性\n\n静态属性(方法),也称为类属性。使用静态属性无需创建实例,通过类即可直接使用\n静态属性(方法)使用static开头\n示例:\nclass Tools{ static PI = 3.1415926; static sum(num1: number, num2: number){ return num1 + num2 }}console.log(Tools.PI);console.log(Tools.sum(123, 456));\n\nthis\n\n在类中,使用this表示当前对象\n\n\n继承\n\n继承时面向对象中的又一个特性\n通过继承可以将其他类中的属性和方法引入到当前类中\n\n示例:\nclass Animal{ name: string; age: number; constructor(name: string, age: number){ this.name = name; this.age = age; }}class Dog extends Animal{ bark(){ console.log(`${this.name}在汪汪叫!`); }}const dog = new Dog('旺财', 4);dog.bark();\n\n通过继承可以在不修改类的情况下完成对类的扩展\n重写\n\n发生继承时,如果子类中的方法会替换掉父类中的同名方法,这就称为方法的重写\n示例:\nclass Animal{ name: string; age: number; constructor(name: string, age: number){ this.name = name; this.age = age; } run(){ console.log(`父类中的run方法!`); }}class Dog extends Animal{ bark(){ console.log(`${this.name}在汪汪叫!`); } run(){ console.log(`子类中的run方法,会重写父类中的run方法!`); }}const dog = new Dog('旺财', 4);dog.bark();\n\n在子类中可以使用super来完成对父类的引用\n\n\n抽象类(abstract class)\n\n抽象类是专门用来被其他类所继承的类,它只能被其他类所继承不能用来创建实例\nabstract class Animal{ abstract run(): void; bark(){ console.log('动物在叫~'); }}class Dog extends Animals{ run(){ console.log('狗在跑~'); }}\n使用abstract开头的方法叫做抽象方法,抽象方法没有方法体只能定义在抽象类中,继承抽象类时抽象方法必须要实现\n\n\n\n6.3 接口(Interface)\n接口的作用类似于抽象类,不同点在于接口中的所有方法和属性都是没有实值的,换句话说接口中的所有方法都是抽象方法。接口主要负责定义一个类的结构,接口可以去限制一个对象的接口,对象只有包含接口中定义的所有属性和方法时才能匹配接口。同时,可以让一个类去实现接口,实现接口时类中要保护接口中的所有属性。\n\n示例(检查对象类型):\ninterface Person{ name: string; sayHello():void;}function fn(per: Person){ per.sayHello();}fn({name:'孙悟空', sayHello() {console.log(`Hello, 我是 ${this.name}`)}});\n示例(实现)\ninterface Person{ name: string; sayHello():void;}class Student implements Person{ constructor(public name: string) { } sayHello() { console.log('大家好,我是'+this.name); }}\n\n6.4 泛型(Generic)\n定义一个函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定),此时泛型便能够发挥作用。\n\n举个例子:\nfunction test(arg: any): any{\treturn arg;}\n\n上例中,test函数有一个参数类型不确定,但是能确定的时其返回值的类型和参数的类型是相同的,由于类型不确定所以参数和返回值均使用了any,但是很明显这样做是不合适的,首先使用any会关闭TS的类型检查,其次这样设置也不能体现出参数和返回值是相同的类型\n使用泛型:\nfunction test<T>(arg: T): T{\treturn arg;}\n这里的<T>就是泛型,T是我们给这个类型起的名字(不一定非叫T),设置泛型后即可在函数中使用T来表示该类型。所以泛型其实很好理解,就表示某个类型。\n那么如何使用上边的函数呢?\n\n方式一(直接使用):\ntest(10)\n\n使用时可以直接传递参数使用,类型会由TS自动推断出来,但有时编译器无法自动推断时还需要使用下面的方式\n\n方式二(指定类型):\ntest<number>(10)\n\n也可以在函数后手动指定泛型\n\n\n可以同时指定多个泛型,泛型间使用逗号隔开:\nfunction test<T, K>(a: T, b: K): K{ return b;}test<number, string>(10, \"hello\");\n\n使用泛型时,完全可以将泛型当成是一个普通的类去使用\n\n类中同样可以使用泛型:\nclass MyClass<T>{ prop: T; constructor(prop: T){ this.prop = prop; }}\n除此之外,也可以对泛型的范围进行约束\ninterface MyInter{ length: number;}function test<T extends MyInter>(arg: T): number{ return arg.length;}\n\n使用T extends\nMyInter表示泛型T必须是MyInter的子类,不一定非要使用接口类和抽象类同样适用。\n\n\n\n","categories":["技术"],"tags":["TypeScript"]},{"title":"前端基础","url":"/2024/12/01/%E5%89%8D%E7%AB%AF/%E5%89%8D%E7%AB%AF%E5%9F%BA%E7%A1%80/","content":"\n浏览器\n\nurl 的组成部分\n\n协议(Protocol):表示访问网页时使用的通信协议,常见的有\nHTTP、HTTPS、FTP 等。\n域名(Domain\nName):表示网站的名称,是网站在互联网上的唯一标识。域名由多个部分组成,包括主域名和子域名,例如www.example.com中的\"www\"是子域名,\"example\"是主域名,\".com\"是顶级域名。顶级域名:也就是后缀,例如.com、.cn等。(备注:域名可以说是一个IP地址的代称,目的是为了便于记忆后者。\n端口号(Port):表示用于访问网站的端口号,默认为\n80。例如,http://www.example.com:8080中的\"8080\"就是端口号。端口号的范围是:0~65535\n路径(Path):表示网站上具体的文件或目录路径。例如,http://www.example.com/path/to/file中的\"/path/to/file\"就是路径(网址可以没有端口号)。\n查询参数(Query\nParameters):表示向服务器传递的参数,用于定制请求的内容。查询参数以\"?\"开头,多个参数之间使用\"&\"分隔。例如,http://www.example.com/path/to/file?param1=value1&param2=value2中的param1=value1&param2=value2就是查询参数,这种常见于项目中路由跳转的传参、get请求等。\n锚点(Anchor):表示网页内部的定位点。锚点以\"#\"开头,用于跳转到网页的特定位置。例如,http://www.example.com/path/to/file#section1中的\"#section1\"就是锚点,常见于a标签的超链接。\n\n输入 url 敲回车后发生了什么\n大厂常问:输入\nURL 到显示页面的全过程\n\n在浏览器地址栏输⼊URL\n浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到解码步骤\n\n如果资源未缓存,发起新请求\n如果已缓存,检验是否⾜够新鲜,⾜够新鲜直接提供给客户端,否则与服务器进⾏验证。\n检验新鲜通常有两个 HTTP 头进⾏控制 Expires 和 Cache-Control:\nHTTP1.0 提供 Expires,值为⼀个绝对时间表示缓存新鲜⽇期 HTTP1.1 增加了\nCache-Control: max-age=time,值为以秒为单位的最⼤新鲜时间\n\n浏览器解析\nURL获取协议,主机,端⼝,路径等信息\n浏览器组装⼀个 HTTP(GET)请求报⽂\n浏览器获取主机 ip\n地址,过程如下:\n\n浏览器缓存\n本机缓存\nhosts⽂件\n路由器缓存\nISP DNS缓存\nDNS递归查询(可能存在负载均衡导致每次IP不⼀样)\n\n开启一个socket与目标 IP 地址端口建立 TCP 连接\n\n三次握手\n\n建立 TCP 后发送 HTTP请求\n服务器接受请求并解析,将请求转发到服务程序\n服务器检查HTTP请求头是否包含缓存验证信息,如果验证缓存新鲜,返回\n304 等对应状态码\n处理程序读取完整请求并准备HTTP响应\n服务器将响应报⽂通过TCP连接发送回浏览器\n浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重⽤\n\n四次挥手\n\n浏览器检查响应状态码\n如果资源可缓存,缓存资源\n对响应进行解码\n根据资源类型确定如何处理\n解析 HTML构建 DOM 树,下载 CSS、JS 资源等,构造\nCSS 规则树,执行 JS 脚本\n\ncookie 和 localStorage\n的区别\n面试官:Javascript\n本地存储的方式有哪些?区别及应用场景? | web 前端面试 -\n面试官系列\n面试官: 既然有了\ncookie 为什么还要 localStorage?😕😕😕Cookie 适合用于在客户端和服务器 -\n掘金\nCookie、LocalStorage\n和 SessionStorage:一次非常详细的对比!_cookie localstorage\nsessionstorage-CSDN 博客\n什么是跨域\n什么是跨域?跨域解决方法-CSDN\n博客\n跨域(Cross-Origin Resource Sharing,简称\nCORS)是一种安全策略,用于限制一个域的网页如何与另一个域的资源进行交互。这是浏览器实现的同源策略(Same-Origin\nPolicy)的一部分,旨在防止恶意网站通过一个域的网页访问另一个域的敏感数据。\n所谓同源,指的是两个页面必须具有相同的协议(protocol)、域名(host)和端口号(port)。\n\n设置 document.domain 解决无法读取非同源网页的 Cookie\n问题\n要求主域名相同\n跨文档通信 API:window.postMessage()\nJSONP\n\nJSONP 是服务器与客户端跨源通信的常用方法\n核心思想:网页通过添加一个<script>元素,向服务器请求\nJSON\n数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。\n\nCORS\n\n普通跨域请求:只需服务器端设置\nAccess-Control-Allow-Origin\n带 cookie 跨域请求:前后端都需要进行设置\n\nAccess-Control-Allow-Credentials\n设置为true\n\n\nWebpack 本地代理(本地调试)\n\n线上环境跨域如何解决\n面试官:聊聊你知道的跨域解决方案跨域是开发中经常会遇到的一个场景,也是面试中经常会讨论的一个问题。掌握常见的跨域解决方案\n- 掘金\n\n通过设置 CORS\nJSONP\nnginx 反向代理\n\n重排(回流)、重绘、如何避免\n面试官:怎么理解回流跟重绘?什么场景下会触发?\n| web 前端面试 - 面试官系列\n事件循环、宏微任务都有哪些\n面试官:说说你对事件循环的理解\n| web 前端面试 - 面试官系列\nJS\n是单线程的,如果需要处理异步任务,不能在异步任务那里阻塞住,因此有了事件循环。\n首先将任务分为同步任务和异步任务,如果是同步任务则直接推入主线程去执行,异步任务则会加入到异步任务队列。当主线程执行结束后,会从任务队列中读取对应的任务,推入到主线程执行。这个过程重复执行就构成了事件循环。\n而任务队列又分为宏任务队列和微任务队列,执行时会优先执行微任务队列的任务直至清空,然后取一个宏任务执行,宏任务结束后再次清空微任务队列的任务,循环往复。\n浏览器的事件循环和\nNodeJS 的事件循环的区别\n面试官:说说对\nNodejs 中的事件循环机制理解? | web 前端面试 - 面试官系列\n在浏览器事件循环中,是根据HTML5定义的规范来实现。而NodeJS的事件循环是基于libuv实现的,libuv是一个多平台的异步\nIO 库。\nNodejs 事件循环分为六个阶段\n\n\n定时器检测阶段(timers):本阶段执行 timer 的回调,即\nsetTimeout、setInterval 里面的回调函数\nI/O 事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O\n回调,即上一轮循环中未被执行的一些 I/O 回调\n闲置阶段(idle, prepare):仅系统内部使用\n轮询阶段(poll):检索新的 I/O 事件;执行与 I/O\n相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和\nsetImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞\n检查阶段(check):setImmediate() 回调函数在这里执行\n关闭事件回调阶段(close\ncallback):一些关闭的回调函数,如:socket.on('close', ...)\n\n每个阶段对应一个队列,当事件循环进入某个阶段时,\n将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行,\n那么将进入下一个处理阶段,直观表现是,宏任务队列全部执行完才会执行微任务队列,而浏览器是宏任务执行一个就去执行微任务队列。\n除了上述 6\n个阶段,还存在process.nextTick,其不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡,\n即本阶段执行结束, 进入下一个阶段前, 所要执行的回调,类似插队\n浏览器缓存\n浏览器缓存机制\nHTTP 和 HTTPS 的区别\n面试官:什么是\nHTTP? HTTP 和 HTTPS 的区别? | web 前端面试 - 面试官系列\nReact\nReact Fiber 是什么\n一问读懂 React\nFiber\nReact 网络请求放在哪里合适\n网络请求放在哪里最合适\n\n放在componentDidMount和componentWillMount有什么区别\n\n推荐放在componentDidMount中\n原因:\n\ncomponentWillMount生命周期在新版中已经过时或者被废弃\nReact 官方推荐在componentDidMount中发送网络请求\n如果使用服务端渲染,componentWillMount这个生命周期的网络请求会发送两次,一次在服务端,一次在客户端,而componentDidMount没有这个问题,只会在客户端请求\n如果组件 render\n时发生了异常导致不能正常挂载,componentDidMount则不会发送请求,而componentWillMount会发送请求,请求到的数据无法被使用,浪费资源\n\n使用 Hooks\n相比于类组件的优势\n\n简化代码\n\n类组件需要定义构造函数、生命周期方法等,而函数式组件只需要函数本身,减少了很多样板代码\n\n易于组合和重用逻辑\n\n使用 Hooks 可以容易地提取和复用组件中的逻辑\n\n避免了 this\n\n减少了 this 的复杂性,代码更清晰\n\n\nJSX 如何编译渲染到页面中\n\n浏览器不能识别 jsx 代码,需要转成 js 代码。\n首先通过 Babel 转换工具将 jsx 转换为 js 代码\nconst element = <h1>Hello, world!</h1>;// 转换后,分别是 标签的类型、标签的属性值对象、标签的内容(子元素)const element = React.createElement('h1', null, 'Hello, world!');\nReact.createElement创建的对象也叫做虚拟 DOM\n将虚拟 DOM 转为真实 DOM\n使用React.createRoot创建根对象,然后将虚拟 DOM 渲染到 id\n为 root 的 DOM 节点中。\n\nBabel 是什么?转换原理?\nBabel\n的原理-CSDN 博客\nBabel 是 JavaScript 编译器,可以将\nTS、JSX、TSX 等转成需要的代码。\nBabel 内部原理是将 JS 代码转换为 AST,对 AST\n应用各种插件进行处理,最终输出编译后的 JS 代码。\nReact Hooks\n为什么不能放在 if else 中\n讲清楚为什么\nReact Hooks 不能放在条件语句中\n多个 Hooks 在 React\n中是以链表的形式存在的,第一次渲染时会形成一个链表,存储着所有的 Hook\n的信息,后边渲染时都会按照这个链表的信息更新\nHook,如果有判断条件,会导致重新渲染前后的链表不一致,导致状态混乱。\nuseEffect 和\nuseLayoutEffect 的区别\n「React」useEffect\n与 useLayoutEffect 使用与区别_useeffect 和 uselayouteffect 使用场景-CSDN\n博客\n\nuseEffect\n\n组件渲染到屏幕后异步执行\n全部 dom 更新完成、浏览器绘制之后异步执行\n不会阻塞页面渲染\n\nuseLayoutEffect 和 useEffect 类似,但也有区别\n\n全部 dom\n更新完成后同步执行,在浏览器绘制前执行\n会阻塞浏览器渲染\n\n一般推荐默认使用\nuseEffect,只有在涉及到需要在布局渲染阶段同步执行的 DOM\n操作或有严格的顺序要求时,才使用 useLayoutEffect。\n\n组件中的 key 属性有什么作用\nReact 存在\nDiff算法,而元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染。\n因此key的值需要为每一个元素赋予一个确定的标识\n父子组件的子组件有个\nkey,每次渲染后 key 值+1 后会发生什么\n组件被卸载并重新挂载\n\nReact 使用 key 来唯一标识列表中的每个子组件。\n当 key 改变时,React 会认为这是一个全新的组件。\n结果是,原来的组件实例会被卸载,新的组件实例会被挂载。\n\n组件的状态丢失\n\n如果子组件内部有本地状态(useState 或类组件的\nstate),状态会在每次 key 改变时被重置。\n这是因为 React 认为这是一个新组件实例,旧的状态无法保留。\n\n性能影响\n\n每次 key 改变,React\n会卸载和重新挂载子组件,而不是重用现有的 DOM 和组件实例。\n这会增加不必要的渲染和计算负担,从而影响性能,特别是当子组件包含复杂逻辑或有较多\nDOM 元素时。\n\nReact 性能优化\n面试官:说说你是如何提高组件的渲染效率的?在\nReact 中如何避免不必要的 render? | web 前端面试 - 面试官系列\nshouldComponentUpdate,更新前手动比较更新前后的值,如果相同则返回false不进行更新,否则返回true更新\nPureComponent\nReact.memo,缓存组件,给组件的props参数更新时,才会更新组件,只适用于函数式组件\n\n避免使用内联函数\n\n比如事件回调时,避免在回调时设置类似于箭头函数,而是在外层定义好,直接用函数名代替,避免重复生成函数。\n\n使用 React.Fragments 避免额外标记\n\n空标签或者 fragment\n标签可以作为顶级标签使用,不会像组件引入任何额外标记\n\n使用 Immutable\n\n使用 Immutable可以给 React\n应用带来性能的优化,主要体现在减少渲染的次数\n在做react性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()中做对比,当返回true执行render方法\nImmutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比较\n\n使用 lazy 组件懒加载\n\nContext 和 Redux 的区别\n【react】context\nVS redux 前言 自从新的 context API 和 hook 特性相继出来后,江湖上类似于\n- 掘金\n\nContext\n主要用于解决跨组件数据传递和共享问题,祖先组件通过Context.Provider组件发布数据,后代组件可以通过Context.Consumer或者useContext钩子订阅数据进行消费\nContext + useReducer\n通过Context管理状态,useReducer改变状态,可以简单实现状态管理的功能。\n但是订阅了 context\n实例的组件,当数据更新时,即使这个组件没有消费更新的数据,该组件仍然会重新渲染。\nRedux\n完整的状态管理框架。通过React-Redux管理状态,当组件消费数据时,其他数据更新不会影响到该组件,只有消费的数据更新了,才会重新渲染。\n调试工具\nRedux调试工具可以清楚的看到状态什么时候发生了什么变化,而Context不能看到,只能看到当前的状态\n\nReactContext\nReact\n组件通信方式\n父组件向后代组件传递数据,\n父组件通过React.createContext创建一个context\nconst testContext = React.createContext('test');\n通过testContext.Provider组件包裹后代组件,将要传递的数据放在Provider组件中。\n<testContext.Provider value={100}></testContext.Provider>\n后代组件获取这个数据可以使用testContext.Consumer组件\n<testContext.Consumer> {/* 必须写一个函数,获取的数据放在了函数入参里 */} {(value) => <div>{value}</div>}</testContext.Consumer>\n或者在类组件中设置contextType属性接收\nclass MyClass extend React.Component { static contextType = testContext; render() { // 获取数据了 let value = this.context; }}\nReact 的发布订阅模式\nReact 虚拟 dom diff 操作\n面试官:说说\nReact diff 的原理是什么? | web 前端面试 - 面试官系列\n\ntree diff,层级比较,树中同一个层级的比较,只有添加、删除操作\ncomponent\ndiff,组件比较,组件比较类型,类型不同则直接删除旧的,添加新的(连同子组件一并删除、添加)\nelement diff,元素比较,有删除、添加、移动操作。\n\n有key的情况下可以比较快的移动完成\n主要是index, oldIndex, maxIndex,通过这三个值比较,然后进行移动操作\n按照新集合的顺序进行排列,即index的值为0, 1, 2, ..., n,然后对比旧集合中key,依次将旧集合的位置索引赋值给oldIndex,所以oldIndex可能为2, 0, 1, 3, ..., n,初始值maxIndex设置为\n0\n按照如下规则对index进行遍历\n\noldIndex > maxIndex时,令maxIndex = oldIndex\noldIndex === maxIndex时,不操作\noldIndex < maxIndex时,将oldIndex位置的元素移动到index的位置。\n\n\n\nReact render 函数理解\n面试官:说说\nReact render 方法的原理?在什么时候会被触发? | web 前端面试 -\n面试官系列\nrender函数里面可以编写JSX,转化成createElement这种形式,用于生成虚拟DOM,最终转化成真实\nDOM\n在React 中,类组件只要执行了\nsetState 方法,就一定会触发 render\n函数执行,函数组件使用useState更改状态不一定导致重新render\n组件的props 改变了,不一定触发 render\n函数的执行(父组件使用useRef传参,父组件更新 ref\n对象,但是子组件不会重新渲染),但是如果 props\n的值来自于父组件或者祖先组件的\nstate,在这种情况下,父组件或者祖先组件的\nstate 发生了改变,就会导致子组件的重新渲染\n所以,类组件一旦执行了setState就会执行render方法,函数式组件useState\n会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染\n\n在组件生命周期或 React 合成事件中,setState 是异步\n在 setTimeout 或者原生 dom 事件中,setState 是同步\n\nReact 生命周期\nreact\n生命周期总结(旧、新生命周期及 Hook)-腾讯云开发者社区-腾讯云\nshouldComponentUpdate\n周期有什么作用?\nCSS\n对于 CSS 预编译语言的理解\n面试官:说说对\nCss 预编语言的理解?有哪些区别? | web 前端面试 - 面试官系列\n扩充了 CSS,增加了变量、函数等功能。可以认为是 CSS\n的超集。不同的预编译语言有不同的解析器,最终这些都会被编译成对应的 CSS\n文件。\n主要有 sass, less, stylus,三种。\n扩充了一些功能,如\n\n变量\n\nsass:$red: #abc使用$开头,冒号分隔\nless: @red: #abc使用@开头,冒号分隔\nstylus: red = #abc直接定义,等号分隔\n\n作用域\n混入\n\n将一部分样式抽离出来,然后被重复使用\n\n代码模块化\n\n实现三栏布局\n7\n种方式实现三栏布局\nposition 每个属性和作用\nJavaScript\n对于 Promise 的理解\nPromise\n的理解\nPromise是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大\n\n链式操作减低了编码难度\n代码可读性明显增强\n\n有三种状态:\n\npending(等待中)\nfulfilled(成功)\nrejected(失败)\n\n从pending变为fulfilled或者rejected后,就不会再改变\nasync 异步函数如何捕获异常\nasync function fn() { await fetchData();}fn();\n当执行fn函数时fetchData可能会抛出异常,如何捕获异常?\n1. try-catch\nasync function fn() { try { await fetchData(); } catch (error) { console.log(error); }}fn();\n2. 内部catch\nasync function fn() { const data = await fetchData().catch((e) => console.log(e)); // 如果有异常,则data无数据 if (!data) return;}fn();\n3. 外部catch\nasync function fn() { await fetchData();}fn().catch((err) => console.log(err));\n闭包是什么\n闭包的理解\n一个函数和对其周围状态的引用捆绑在一起,这个组合就形成了闭包,换句话说,闭包可以在函数内部访问到函数外部的作用域。\n闭包可以:\n\n创建私有变量\n延长变量的生命周期\n\n通常的使用场景:\n\n计数器\n函数柯里化\n防抖\n\n有哪些模块规范\n深入对比\nesModule 和 commonjs 模块化的区别前言 commonjs 2009 年,Ryan Dahl\n基于开源的 V - 掘金\n主要有commonJS, AMD, CMD, ESModule,现在主要使用的是commonJS, ESModule。\n两者的区别:\n\n引入方式\n\ncommonJS是动态导入,可以在任何地方引入\nESModule模块是静态导入(在编译阶段进行导入),不能动态加载语句,所以\nimport 不能写在块级作用域和判断条件内\n\n使用语法\n\ncommonJS是通过module.exports,\nrequire导出和导入\nesModule通过import, export导入导出\n\n模块导入方式\n\ncommonJS是运行一遍代码,将module.exports的值赋值给变量\nesModule模块输出的是一个值的引用,\n使用的是动态绑定,esModule\n导入导出的值都指向同一个内存地址,所以导入值会跟着导出值发生变化\n\n加载模式\n\ncommonJS是同步加载\nesModule是异步加载\n\n\n区别原因:\n\n同步加载会导致浏览器出现卡顿的情况。而 node\n大多运行在服务端,同步加载本地文件速度很快。浏览器加载模块通常是网络请求,同步的话会出现卡顿的情况。\n\nJS 的有哪些数据类型\n\n基本类型\nNumber, String, Boolean,\nUndefined, null, Symbol\n复杂类型\n统称为 object 类型,常见的有Object, Array,\nFunction\n\n面试官:说说\nJavaScript 中的数据类型?存储上的差别? | web 前端面试 -\n面试官系列\n实现一个\nrequest,失败后间隔 interval 重试,设置最大重试次数\nasync function request(url, options, interval, maxCount) { // 重试次数 let retries = 0; while (retries < maxCount) { try { // await会阻塞,直到返回 const response = await fetch(url, options); return response; } catch (error) { // 如果有错误,说明请求失败,重试 retries++; // 这里使用await阻塞,表示间隔时间 await new Promise((resolve) => setTimeout(resolve, interval)); } }}\n字符串 slice, substring,\nsubstr 的区别\nslice, substring,\nsubstr 的区别\nnew 关键字的执行过程\n面试官:说说\nnew 操作符具体干了什么? | web 前端面试 - 面试官系列\n\n创建一个空对象\n新对象原型指向构造函数的原型对象\n将新对象设置为构造函数的this,执行构造函数\n函数返回值不为对象或者是null,则返回新对象,否则返回函数的返回值\n\nJS 的垃圾回收\n深入了解\nJavaScript 垃圾回收机制_javascript 的垃圾回收机制讲一下-CSDN\n博客\nES6 常用 api\nasync, await 实现原理\nasync 表示声明一个异步函数,这个函数返回值是一个 Promise\n对象,如果返回值不是 Promise 类型,则会将返回值包装成 Promise\n类型。等同于 Promise.resolve(x)。\nawait 表示等待的意思,可以等待异步函数也就是 Promise\n对象,也可以等待普通的变量值。并且只能在 async 函数中使用。\n使用 yield 实现 async,核心原理是递归迭代执行 next\nfunction myAsync(gen) { return function (...args) { return new Promise((resolve, reject) => { const iter = gen(...args); const step = (arg) => { const { value, done } = iter.next(arg); if (done) { return resolve(value); } Promise.resolve(value).then((val) => step(val)); }; step(); }); };}\nasync/await 可以看作是生成器的语法糖。\n将生成函数的 * 替换成 async,将 yield 替换成 await\nvar, let, const 之间的区别\n面试官:说说\nvar、let、const 之间的区别 | web 前端面试 - 面试官系列\n\n变量提升\nvar有变量提升,let, const没有\n暂时性死区\nvar没有暂时性死区,let, const有,只有执行到let, const那行以后才可以使用变量。\n块级作用域\nvar没有块级作用域,在作用域内声明的,作用域外也可以使用。let, const在作用域内声明的不能在作用域外使用。\n重复声明\nvar可以重复声明,let, const在同一个作用域内不可重复声明\n修改声明的变量\nvar, let可以修改声明的变量,const声明时必须初始化,且后边不可修改。\n\n事件流\n事件与事件流\n事件:通常是使用 js 与浏览器 HTML\n文档进行交互的操作,如点击事件等。\n事件流三个阶段:\n\n事件捕获阶段\n事件处理阶段\n事件冒泡阶段\n\n\n原始事件模型\n\n绑定方式简单,可以在 HTML 代码中直接绑定,也可以通过 js 代码绑定\n<input type=\"button\" onclick=\"func()\" />\nconst rootEl = document.getElementById('root');rootEl.onclick = func;\n\n只支持事件冒泡,不支持捕获\n同一个类型事件只能绑定一次,后边的会覆盖前边的\n绑定速度快\n\n\n标准事件模型\n\n通过addEventListener和removeEventListener添加和删除事件监听,参数分别为eventType, handler, useCapture,表示事件类型、回调函数、是否捕获(true表示在捕获阶段执行,false表示在冒泡阶段执行)。\nconst rootEl = document.getElementById('root');// 冒泡阶段执行rootEl.addEventListener('click', handleClick, false);// 捕获阶段执行rootEl.addEventListener('click', handleClick, true);\n\n支持事件捕获、事件处理、事件冒泡三个阶段\n支持绑定多个事件,不冲突\n\n\nIE 事件模型(基本不用)\n\n分为事件处理阶段和事件冒泡阶段\n事件冒泡\n父子元素,子元素的事件会将该事件层层上报,使得父元素也发生该事件\n事件委托\n面试官:解释下什么是事件代理?应用场景?\n| web 前端面试 - 面试官系列\nJS\n中的事件冒泡、事件捕获、事件委托 DOM 事件流(event flow\n)存在三个阶段:事件捕获阶段、处于目标阶段、事件 - 掘金\nJS 如何实现异步操作\n详解\nJS\n的四种异步解决方案:回调函数、Promise、Generator、async/await(干货满满)...-CSDN\n博客\n\n最开始通过设置回调函数的形式\nPromise\nasync、await\n\n判断对象是不是数组\nJS\n判断是否为对象或数组的几种方法_js 判断是否是对象-CSDN 博客\n\nArrays.isArray()\nval instanceof Array\nval?.constructor === Array\nisPrototypeOf,判断一个对象是不是在另一个对象的原型链上\nArray.prototype.isPrototypeof(val)\n\nsourcemap\nblog.csdn.net/weixin_40599109/article/details/107845431\n主要是映射转换后的代码和源码的关系,方便做调试。\n操作系统\n进程和线程和协程的区别\n进程之间的通信方式\n算法和数据结构\nDFS 和 BFS\n区别、时间空间复杂度、应用场景\nDFS、BFS\n空间时间复杂度分析 - wkfxm - 博客园\n","categories":["前端"],"tags":["前端","八股"]},{"title":"Hive学习","url":"/2023/12/01/%E5%A4%A7%E6%95%B0%E6%8D%AE/Hive%E5%AD%A6%E4%B9%A0/","content":"查询\n每个Reduce内部排序(sort by)\nSort By:对于大规模的数据集order\nby的效率非常低。在很多情况下,并不需要全局 排序,此时可以使用Sort\nby。\n\nSort\nby为每个reduce产生一个排序文件。每个Reduce内部进行排序,对全局结果集\n来说不是排序。\n1)设置reduce个数\nhive (default)> set mapreduce.job.reduces=3;\n2)查看设置reduce个数\nhive (default)> set mapreduce.job.reduces;\n3)根据部门编号降序查看员工信息\nhive (default)> select * from emp sort by deptno desc;\n4)将查询结果导入到文件中(按照部门编号降序排序)\nhive (default)> insert overwrite local directory '/opt/module/hive/datas/sortby-result' select * from emp sort by deptno desc;\n分区\nDistribute By:在有些情况下,我们需要控制某个特定行应该到哪个\nReducer,通常是 为了进行后续的聚集操作。distribute\nby子句可以做这件事。distribute by类似MapReduce\n中partition(自定义分区),进行分区,结合sort by使用。\n对于distribute by 进行测试,一定要分配多reduce\n进行处理,否则无法看到distribute by 的效果。\n(1)先按照部门编号分区,再按照员工编号薪资排序\nhive (default)> set mapreduce.job.reduces=3;\nhive (default)> insert overwrite local directory '/opt/module/hive/datas/distribute-result' select * from emp distribute by deptno sort by sal desc;\n注意:\n➢ distribute\nby的分区规则是根据分区字段的hash码与reduce的个数进行相除后,\n余数相同的分到一个区。\n➢ Hive要求distribute by语句要写在sort by语句之前。\n➢\n演示完以后mapreduce.job.reduces的值要设置回-1,否则下面分区or分桶表load\n跑MapReduce的时候会报错。\ngrouping sets\ncube\n数据立方体-Hive\nCube-CSDN博客\nHive中with\ncube、with rollup、grouping sets用法_hive sum with\nrollup-CSDN博客\n函数\n单行函数\nnvl:替换null值\nnvl(A, B)若A为null则返回B,否则返回A\nconcat_ws:以指定分隔符拼接字符串或者字符串数组\n语法:concat_ws(string A, string…| array(string))\n返回值:string\n说明:使用分隔符A拼接多个字符串,或者一个数组的所有元素。\nget_json_object:解析 json\n字符串\n语法:get_json_object(string json_string, string path)\n返回值:string\n说明:解析json的字符串json_string,返回path指定的内容。如果输入的json字符串\n无效,那么返回NULL。\nhive> select get_json_object('[{\"name\":\"大海海\",\"sex\":\"男\",\"age\":\"25\"},{\"name\":\"小宋宋\",\"sex\":\"男\",\"age\":\"47\"}]','$.[0].name'); hive> 大海海\nhive> select get_json_object('[{\"name\":\"大海海\",\"sex\":\"男\",\"age\":\"25\"},{\"name\":\"小宋宋\",\"sex\":\"男\",\"age\":\"47\"}]','$.[0]');hive> {\"name\":\"大海海\",\"sex\":\"男\",\"age\":\"25\"}\nunix_timestamp:返回当前或指定时间的时间戳\n语法:unix_timestamp()\n返回值:bigint\nhive> select unix_timestamp('2022/08/08 08-08-08','yyyy/MM/dd HH-mm-ss'); hive> 1659946088\nfrom_unixtime:转化 UNIX\n时间戳\n(从 1970-01-01 00:00:00 UTC\n到指定时间的秒数)到当前时区的时间格式\n语法:from_unixtime(bigint unixtime[, string format])\n返回值:string\ncurrent_date:当前日期\ncurrent_timestamp:当前的日期加时间,并且精确的毫秒\ndate_add:日期加天数\n语法:date_add(string startdate, int days)\n返回值:string\n说明:返回开始日期 startdate 增加 days 天后的日期\ndate_sub:日期减天数\n语法:date_sub (string startdate, int days)\n返回值:string\n说明:返回开始日期startdate减少days天后的日期。\nsize:集合中元素的个数\nhive> select size(friends) from test; --2/2 每一行数据中的friends集合里的个数 \nmap:创建map集合\n语法:map (key1, value1, key2, value2, …)\n说明:根据输入的key和value对构建map类型\nhive> select map('xiaohai',1,'dahai',2); hive> {\"xiaohai\":1,\"dahai\":2}\nmap_keys: 返回map中的key\nhive> select map_keys(map('xiaohai',1,'dahai',2));hive>[\"xiaohai\",\"dahai\"] \nmap_values: 返回 map\n中的value\nhive> select map_values(map('xiaohai',1,'dahai',2));hive>[1,2] \narray 声明 array 集合\n语法:array(val1, val2, …)\n说明:根据输入的参数构建数组array类\narray_contains:\n判断 array 中是否包含某个元素\nhive> select array_contains(array('a','b','c','d'),'a'); hive> true\nsort_array:将 array\n中的元素排序\nhive> select sort_array(array('a','d','c'));hive> [\"a\",\"c\",\"d\"]\nstruct 声明 struct\n中的各属性\n语法:struct(val1, val2, val3, …)\n说明:根据输入的参数构建结构体struct类\nhive> select struct('name','age','weight');hive> {\"col1\":\"name\",\"col2\":\"age\",\"col3\":\"weight\"} \nnamed_struct 声明 struct\n的属性和值\nhive> select named_struct('name','xiaosong','age',18,'weight',80);hive> {\"name\":\"xiaosong\",\"age\":18,\"weight\":80} \n高级聚合函数\ncollect_list\n收集并形成list集合,结果不去重\nhive> select sex, collect_list(job) from employee group by sex 女 [\"行政\",\"研发\",\"行政\",\"前台\"] 男 [\"销售\",\"研发\",\"销售\",\"前台\"]\ncollect_set\n收集并形成set集合,结果去重\nhive> select sex, collect_set(job) from employee group by sex 女 [\"行政\",\"研发\",\"前台\"] 男 [\"销售\",\"研发\",\"前台\"]\n爆炸函数\nexplode是将hive一行中复杂的array或者map结构拆分成多行。\nlateral view用于和split,\nexplode等UDTF一起使用,它能够将一行数据拆成多行数据,在此基础上可以对拆分后的数据进行聚合。lateral\nview首先为原始表的每行调用UDTF,UDTF会把一行拆分成一或者多行,lateral\nview再把结果组合,产生一个支持别名表的虚拟表。\nexplode将复杂结构一行拆成多行,然后再用lateral view做各种聚合。\n\nSELECT\tcate, COUNT(*) cntFROM movie_infoLATERAL VIEW EXPLODE(SPLIT(category, ',')) t1 AS cateGROUP BY cate;\n\n窗口函数\nlag和lead\n功能:获取当前行的上/下边某行的字段的值。\n\nfirst_value和last_value\n功能:获取窗口内第一个值和最后一个值\n\n分区表\ncreate table dept_partition ( deptno int, --部门编号 dname string, --部门名称 loc string --部门位置 ) partitioned by (day string) row format delimited fields terminated by '\\t'; \n装载数据\nload data local inpath '/opt/module/hive/datas/dept_20220401.log' into table dept_partition partition(day='20220401'); \n插入数据\ninsert overwrite table dept_partition partition (day = '20220402') select deptno, dname, loc from dept_partition where day = '2020-04-01'; \n查询数据\nselect deptno, dname, loc ,day from dept_partition where day = '2020-04-01'; \n创建单个分区\nalter table dept_partition add partition(day='20220403'); \n创建多个分区(不能有逗号)\nalter table dept_partition add partition(day='20220404') partition(day='20220405');\n删除一个分区\nalter table dept_partition drop partition (day='20220403'); \n删除多个分区(必须有逗号)\nalter table dept_partition drop partition (day='20220404'), partition(day='20220405');\n二级分区\ncreate table dept_partition2( deptno int, -- 部门编号 dname string, -- 部门名称 loc string -- 部门位置 ) partitioned by (day string, hour string) row format delimited fields terminated by '\\t';\n动态分区\ninsert into table dept_partition_dynamic partition(loc) select deptno, dname, loc from dept;\n无需指定分区的值,自动分区。\n分桶表\ncreate table stu_buck( id int, name string ) clustered by(id) into 4 buckets row format delimited fields terminated by '\\t'; \n分桶排序表\ncreate table stu_buck_sort( id int, name string ) clustered by(id) sorted by(id)into 4 buckets row format delimited fields terminated by '\\t';\nHive文件格式\n为Hive\n表中的数据选择一个合适的文件格式,对提高查询性能的提高是十分有益的。\nHive 表数据的存储格式,可以选择text file、orc、parquet、sequence file\n等。\nORC文件格式\nORC(Optimized Row Columnar)file format 是 Hive 0.11\n版里引入的一种列式存储的文\n件格式。ORC文件能够提高Hive读写数据和处理数据的性能。\n\n(1)行存储的特点\n查询满足条件的一整行数据的时候,列存储则需要去每个聚集的字段找到对应的每个列的值,行存储只需要找到其中一个值,其余的值都在相邻地方,所以此时行存储查询的\n速度更快\n(2)列存储的特点\n因为每个字段的数据聚集存储,在查询只需要少数几个字段的时候,能大大减少读取\n的数据量;每个字段的数据类型一定是相同的,列式存储可以针对性的设计更好的设计压\n缩算法。\n前文提到的text file 和sequence file 都是基于行存储的,orc 和parquet\n是基于列式存 储的。\n\ncreate table orc_table (column_specs) stored as orc tblproperties (property_name=property_value, ...); \n\nParquet文件格式\n\n","categories":["大数据"],"tags":["Hive"]},{"title":"大数据环境搭建","url":"/2023/12/01/%E5%A4%A7%E6%95%B0%E6%8D%AE/%E5%A4%A7%E6%95%B0%E6%8D%AE%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/","content":"1. Hadoop搭建\n1.1 虚拟机准备\n在本地VMware软件中安装三台CentOS 7的虚拟机,充当集群。安装完成后分别配置主机名和IP为如下:\n192.168.10.100 cluster100192.168.10.101 cluster101192.168.10.102 cluster102\n同时关闭所有服务器的防火墙。\n\n1.2 Java安装\n下载jdk1.8版本的Java环境,解压到/opt/module目录中。同时配置环境变量。\n在/etc/profile.d/my_env.sh中添加以下内容:\nexport JAVA_HOME=/opt/module/jdk1.8export PATH=$PATH:$JAVA_HOME/bin\n刷新环境变量。然后执行java -version命令,显示Java版本表示安装成功。\n\n1.3 Hadoop安装\n下载hadoop3.1.3版本的Hadoop压缩包,解压到/opt/module目录中。同时配置环境变量。\n在/etc/prodile.d/my_env.sh中添加一下内容:\nexport HADOOP_HOME=/opt/module/hadoop3.1.3export PATH=$PATH:$HADOOP_HOME/binexport PATH=$PATH:$HADOOP_HOME/sbin\n刷新环境变量。然后执行hadoop version命令,显示版本表示安装成功。\n.kpcelbkjqiwb{zoom: 67%;}\n\n1.4 Hadoop分布式运行模式\n首先将上边安装的Java环境、Hadoop环境和环境变量文件同步到另外两台服务器中。可以使用scp命令发送文件和目录。\n1.4.1 集群规划\n\n\n\n\n\n\n\n\n\n\ncluster100\ncluster101\ncluster102\n\n\n\n\nHDFS\nNameNodeDataNode\nDataNode\nSecondaryNameNodeDataNode\n\n\nYARN\nNodeManager\nResourceManagerNodeManager\nNodeManager\n\n\n\n1.4.2 集群配置\n集群配置主要有4个配置文件,都存储在$HADOOP_HOME/etc/hadoop目录中,分别是core-site.xml、hdfs-site.xml、yarn-site.xml和mapred-site.xml,这四个配置文件分别对应hadoop核心配置、hdfs配置、yarn配置和map\nreduce配置。\n1.4.2.1 核心配置\n编辑core-site.xml,在<configuration></configuration>添加以下内容:\n<!-- 指定NameNode的地址 --><property> <name>fs.defaultFS</name> <value>hdfs://cluster100:8020</value></property> <!-- 指定hadoop数据的存储目录 --><property> <name>hadoop.tmp.dir</name> <value>/opt/module/hadoop3.1.3/data</value> </property> <!-- 配置HDFS网页登录使用的静态用户为lishan --><property> <name>hadoop.http.staticuser.user</name> <value>lishan</value></property>\n1.4.2.2 HDFS配置\n编辑hdfs-site.xml,添加以下内容:\n<!-- NameNode web端访问地址--><property> <name>dfs.namenode.http-address</name> <value>cluster100:9870</value></property><!-- SecondaryNameNode web端访问地址--><property> <name>dfs.namenode.secondary.http-address</name> <value>cluster102:9868</value></property>\n1.4.2.3 YARN配置\n编辑yarn-site.xml,添加以下内容:\n<!-- 指定MR走shuffle --><property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property> <!-- 指定ResourceManager的地址--> <property> <name>yarn.resourcemanager.hostname</name> <value>cluster101</value></property> <!-- 环境变量的继承 --> <property> <name>yarn.nodemanager.env-whitelist</name><value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value></property>\n1.4.2.4 MapReduce配置\n编辑mapred-site.xml,添加以下内容:\n<!-- 指定MapReduce程序运行在Yarn上 --><property> <name>mapreduce.framework.name</name> <value>yarn</value></property>\n1.4.2.5 节点配置\n编辑$HADOOP_HOME/etc/hadoop目录下的workers文件,把所有节点添加至文件中:\ncluster100cluster101cluster102\n注意:该文件中不得有空行,每行末尾不允许有空格\n1.4.2.6 分发配置\n将配置好的文件分发到其他服务器(cluster101 cluster102)。\n1.4.3 集群启动\n如果是第一次启动,需要在NameNode所在节点(cluster100)格式化NameNode。执行命令hdfs namenode -format格式化。\n\n打印该日志信息表示格式化成功。\n\nhdfs启动\n使用$HADOOP_HOME/sbin/start-dfs.sh启动hdfs,由于配置了环境变量,也可以直接使用start-dfs.sh命令。\n打印出以下信息表示启动成功。\n\n然后输入jps,查看正在运行的进程。\n\n可以看到DataNode和NameNode都启动了。\n在cluster102中也查看进程,可以看到SecondNameNode也启动了,符合最开始的规划。\n\n启动Yarn\n设计的Yarn需要配置在cluster101中,所以需要在cluster101中启动yarn。使用命令start-yarn.sh启动。\n启动后使用jps查看进程。\n\n可以看到ResourceManager启动了。\n查看信息\n进入192.168.10.100:9870查看HDFS的相关信息。\n.ezikanfnfwqk{zoom: 33%;}\n\n进入192.168.10.101:8088查看Yarn相关信息。\n.kmycwggobdpc{zoom: 33%;}\n\n展示以上页面表明安装成功,hdfs和yarn已经成功启动。\n\n1.4.4 配置历史服务器\n该服务器可以查看程序的历史运行情况。\n编辑mapred-site.xml文件,添加一下内容:\n<!-- 历史服务器端地址 --> <property> <name>mapreduce.jobhistory.address</name> <value>cluster100:10020</value> </property> <!-- 历史服务器web端地址 --> <property> <name>mapreduce.jobhistory.webapp.address</name> <value>cluster100:19888</value> </property> \n配置好后将该配置文件分发到其他节点。然后使用命令mapred --daemon start historyserver启动历史服务器。\n通过配置的端口访问Web页面,正常显示页面,证明历史服务器已经启动。\n.kcrhpgmxixnr{zoom: 33%;}\n\n1.4.5 配置日志聚集功能\n配置日志聚集可以将所有服务器的运行日志聚集到HDFS中,方便统一查看。\n配置yarn-site.xml,添加以下内容:\n<!-- 开启日志聚集功能 --> <property> <name>yarn.log-aggregation-enable</name> <value>true</value> </property> <!-- 设置日志聚集服务器地址 --> <property> <name>yarn.log.server.url</name> <value>http://cluster10:19888/jobhistory/logs</value> </property> <!-- 设置日志保留时间为7天 --> <property> <name>yarn.log-aggregation.retain-seconds</name> <value>604800</value> </property> \n将配置分发到其他节点。\n注意:配置日志聚集功能,需要重新启动NodeManager、ResourceManager和HistoryServer。\n需要在cluster101中使用stop-yarn.sh关闭yarn,然后使用mapred --daemon stop historyserver关闭历史服务器。\n2. Zookeeper安装\n2.1 下载\n去官网(https://zookeeper.apache.org/)下载Zookeeper的tar包,这里选择3.5.7的版本。\n采用集群部署方式安装Zookeeper,规划在三台节点都安装上Zookeeper。\n首先将下载好的tar包解压到/opt/module目录中。\ntar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/\n2.2 安装\n\n配置服务器编号\n在/opt/module/zookeeper3.5.7目录中创建目录zkData,并在这个目录新建myid文件。\n写入内容为:\n0\n注意:不能空行,末尾不能有空格。\n分发zookeeper到其他服务器。然后分别在cluster101修改myid内容为1,cluster102修改为2。\n配置zoo.cfg文件\n重命名zookeeper3.5.7/conf\n目录下的zoo_sample.cfg为zoo.cfg。修改数据存储位置:\ndataDir=/opt/module/zookeeper3.5.7/zkData\n并添加以下内容:\n#######################cluster########################## server.0=cluster100:2888:3888 server.1=cluster101:2888:3888 server.2=cluster102:2888:3888 \nserver.A=B:C:D表示A号服务器的地址是B,Fllower服务器与集群中的Leader服务器使用C端口交换信息,如果集群中的Leader服务器崩溃,使用D号端口重新选举Leader。其中A即为myid中写入的编号。\n同步该配置文件到其他服务器。\n集群启动\n分别在三台节点执行bin/zkServer.sh start。\n显示如下信息表示启动成功。\n.mmpcfyxbydgu{}\n\n\n.ysvsuluvzeqc{}\n\n\n3. Hbase安装\n3.1 下载\n首先去官网下载HBase安装包,这里选择的是2.4.11的版本。\n解压到/opt/module目录中,并修改名字为:hbase2.4.11。\n并在/etc/profile.d/my_env.sh添加环境变量\nexport HBASE_HOME=/opt/module/hbase2.4.11export PATH=$PATH:$HBASE_HOME/bin\n3.2 安装\n3.2.1 配置\n\n配置hbase-env.sh\n修改conf/hbase-env.sh中的内容export HBASE_MANAGES_ZK=false\n修改hbase-site.xml\n <property> <name>hbase.zookeeper.quorum</name> <value>clister100,cluster101,cluster102</value> <description>The directory shared by RegionServers. </description> </property> <property> <name>hbase.rootdir</name> <value>hdfs://clister100:8020/hbase</value> <description>The directory shared by RegionServers. </description> </property> <property> <name>hbase.cluster.distributed</name> <value>true</value> </property> \n修改regionservers\n修改为如下内容:\ncluster100cluster101cluster102\n解决兼容性问题\n解决hbase和hadoop的log4j兼容性问题,修改hbase的jar包使用hadoop的jar包。\nmv /opt/module/hbase2.4.11/lib/client-facing-thirdparty/slf4j-reload4j-1.7.33.jar /opt/module/hbase2.4.11/lib/client-facing-thirdparty/slf4j-reload4j-1.7.33.jar.bak \n\n将修改好的hbase分发到其他服务器。\n3.2.2 启动\n执行命令start-hbase.sh。显示一下信息启动成功。\n访问192.168.10.100:16010出现hbase的页面,启动成功。\n\n.wjkfylqrwiov{zoom:50%;}\n\n4. MySQL安装\n下载mysql-xxx.tar安装包和mysql驱动jar包。\n4.1 安装\n\n解压安装包\n卸载自带的mariadb\nsudo rpm -qa | grep mariadb | xargs sudo rpm -e --nodeps\n安装MySQL依赖\nsudo rpm -ivh mysql-community-common-5.7.28-1.el7.x86_64.rpmsudo rpm -ivh mysql-community-libs-5.7.28-1.el7.x86_64.rpmsudo rpm -ivh mysql-community-libs-compat-5.7.28-1.el7.x86_64.rpm\n安装client和server\nsudo rpm -ivh mysql-community-client-5.7.28-1.el7.x86_64.rpmsudo rpm -ivh mysql-community-server-5.7.28-1.el7.x86_64.rpm\n启动\nsystemctl start mysqld\n查看msyql密码\nsudo cat /var/log/mysqld.log | grep password\n\n4.2 配置\n\n密码配置\n个人学习可以配置一个简单的密码。\nset global validate_password_policy=0;set global validate_password_length=4;\n设置密码验证策略为最低。\n设置密码\nset password=password(\"123456\");\n修改登录权限\nupdate user set host=\"%\" where user=\"root\";\n刷新权限\nflush privileges;\n\n5. Hive安装\n在官网下载hvie 3.1.3版本tar包。\n5.1 解压\n解压tar包到/opt/module中并修改名字为hive3.1.3\ntar -zxvf apache-hive-3.1.3-bin.tar.gz -C /opt/module/\n添加环境变量\nexport HIVE_HOME=/opt/module/hive3.1.3export PATH=$PATH:$HIVE_HOME/bin\n5.2 配置元数据存储到Mysql中\n\n登录MySQL新建元数据库\ncreate database metastore;\n设置jdbc驱动\n将MySQL的jdbc驱动拷贝到hive的lib目录下\ncp /opt/software/mysql-connector-java-5.1.37-bin.jar /opt/module/hive3.1.3/lib/\n配置hvie-site.xml\n在conf目录下新建hive-site.xml文件,添加如下内容:\n<?xml version=\"1.0\"?><?xml-stylesheet type=\"text/xsl\" href=\"configuration.xsl\"?><configuration> <!-- jdbc连接的URL --> <property> <name>javax.jdo.option.ConnectionURL</name> <value>jdbc:mysql://cluster100:3306/metastore?useSSL=false</value> </property> <!-- jdbc连接的Driver--> <property> <name>javax.jdo.option.ConnectionDriverName</name> <value>com.mysql.jdbc.Driver</value> </property> \t<!-- jdbc连接的username--> <property> <name>javax.jdo.option.ConnectionUserName</name> <value>root</value> </property> <!-- jdbc连接的password --> <property> <name>javax.jdo.option.ConnectionPassword</name> <value>123456</value> </property> <!-- Hive默认在HDFS的工作目录 --> <property> <name>hive.metastore.warehouse.dir</name> <value>/user/hive/warehouse</value> </property></configuration>\n初始化\n初始化hive源数据库。\nschematool -dbType mysql -initSchema -verbose\n如果出现报错:\n.xxbeksmrqpwa{zoom: 50%;}\n\n是包版本的问题,将hadoop中相关的guava复制到hive中即可。\nrm hive3.1.3/lib/guava-19.0.jar cp hadoop3.1.3/share/hadoop/common/lib/guava-27.0-jre.jar hive3.1.3/lib/\n\n打印以上信息表示初始化成功。\n登录MySQL数据库,查看metastore数据库中的所有表。\n.hgxkghrlmyvb{zoom: 50%;}\n\n可以看到有了元数据信息。\n\n5.3 Hive服务部署\n5.3.1 hiveserver2部署\n修改hadoop下的core-site.xml配置文件。增加如下配置\n<!--配置所有节点的lishan用户都可作为代理用户--><property> <name>hadoop.proxyuser.lishan.hosts</name> <value>*</value></property><!--配置lishan用户能够代理的用户组为任意组--><property> <name>hadoop.proxyuser.lishan.groups</name> <value>*</value></property><!--配置lishan用户能够代理的用户为任意用户--><property> <name>hadoop.proxyuser.lishan.users</name> <value>*</value></property>\n修改hive下的hive-site.xml,增加如下信息:\n<!-- 指定hiveserver2连接的host --><property>\t<name>hive.server2.thrift.bind.host</name>\t<value>cluster100</value></property><!-- 指定hiveserver2连接的端口号 --><property>\t<name>hive.server2.thrift.port</name>\t<value>10000</value></property>\n使用命令hive --service hiveserver2启动hiveserver2,然后在另一个终端执行beeline -u jdbc:hive2://cluster100:10000 -n lishan后看到如下信息:\n.hexuqnfifgea{zoom: 67%;}\n\n启动成功。\n5.3.2 metastore服务\n修改hive下的hive-site.xml,添加内容:\n<!-- 指定metastore服务的地址 --><property>\t<name>hive.metastore.uris</name>\t<value>thrift://cluster100:9083</value></property>\n启动metastore服务,hive --service metastore,在另一个终端启动hive,查看数据库,正常访问。\n6. Spark安装\n6.1 下载\n这里下载的spark 3.0.0版本。\n解压到/opt/module中,并修改名为:spark3.0.0。\n6.2 Yarn模式配置\n修改hadoop下的yarn-site.xml文件,添加内容:\n<!--是否启动一个线程检查每个任务正使用的物理内存量,如果任务超出分配值,则直接将其杀掉,默认是true --> <property> <name>yarn.nodemanager.pmem-check-enabled</name> <value>false</value> </property> <!--是否启动一个线程检查每个任务正使用的虚拟内存量,如果任务超出分配值,则直接将其杀掉,默认是true --> <property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property> \n将spark下的conf/spark-env.sh.template重命名为spark-evn.sh并添加:\nexport JAVA_HOME=/opt/module/jdk1.8YARN_CONF_DIR=/opt/module/hadoop3.1.3/etc/hadoop\n分发到所有服务器。\n6.3 配置历史服务器\n重命名spark目录下的mv conf/spark-defaults.conf.template conf/spark-defaults.conf。并修改该文件:\nspark.eventLog.enabled truespark.eventLog.dir hdfs://cluster100:8020/directory\n然后在hdfs上新建该目录:hadoop fs -mkdir /directory\n修改spark-env.sh,添加日志配置。\nexport SPARK_HISTORY_OPTS=\"-Dspark.history.ui.port=18080-Dspark.history.fs.logDirectory=hdfs://cluster100:8020/directory-Dspark.history.retainedApplications=30\"\n修改spark-defaults.conf文件\nspark.yarn.historyServer.address=cluster100:18080spark.history.ui.port=18080\n启动历史服务sbin/start-history-server.sh\n7. Kafka安装\n7.1下载\n下载版本为:kafka 2.12-3.0.0。解压到/opt/module并修改名kafka3.0.0。\n添加环境变量\nexport KAFKA_HOME=/opt/module/kafka3.0.0export PATH=$PATH:$KAFKA_HOME/bin\n7.2配置\n进入kafka目录,修改config/server.properties文件,修改以下内容:\n#broker的全局唯一编号,不能重复,只能是数字。broker.id=0#处理网络请求的线程数量num.network.threads=3#用来处理磁盘IO的线程数量num.io.threads=8#发送套接字的缓冲区大小socket.send.buffer.bytes=102400#接收套接字的缓冲区大小socket.receive.buffer.bytes=102400#请求套接字的缓冲区大小socket.request.max.bytes=104857600#kafka运行日志(数据)存放的路径,路径不需要提前创建,kafka自动帮你创建,可以配置多个磁盘路径,路径与路径之间可以用\",\"分隔log.dirs=/opt/module/kafka3.0.0/datas#topic在当前broker上的分区个数num.partitions=1#用来恢复和清理data下数据的线程数量 num.recovery.threads.per.data.dir=1 # 每个topic创建时的副本数,默认时1个副本 offsets.topic.replication.factor=1 #segment文件保留的最长时间,超时将被删除 log.retention.hours=168#每个segment文件的大小,默认最大1Glog.segment.bytes=1073741824# 检查过期数据的时间,默认5分钟检查一次是否数据过期log.retention.check.interval.ms=300000#配置连接Zookeeper集群地址(在zk根目录下创建/kafka,方便管理)zookeeper.connect=cluster100:2181,cluster101:2181,cluster102:2181/kafka\n分发kafka和环境变量文件到其他服务器。修改cluster101的borker.id为1,cluster102的borker.id为2.\n7.3 启动\n在zookeeper集群启动的情况下启动kafka。\n在每个节点上分别启动kafka。\nbin/kafka-server-start.sh -daemon config/server.properties\n使用jps查看进程\n.uvlwiatdkshk{zoom:67%;}\n\n可以看到kafka已经启动了。\n8. Storm安装\n下载storm 1.2.4版本。并解压到/opt/module中修改名为storm1.2.4。\n8.1 配置\n修改conf下的storm.yaml文件。\nstorm.zookeeper.servers: - \"cluster100\" - \"cluster101\" - \"cluster102\"nimbus.host: \"cluster100\"storm.local.dir: \"/opt/module/storm1.2.4/data\"ui.port: 8888supervisor.slots.ports: - 6700 - 6701 - 6702\n将storm分发到其他节点\n8.2 启动\n在cluster100启动:\nnohup /opt/module/storm1.2.4/bin/storm nimbus > /opt/module/storm1.2.4/output.log 2>&1 &nohup /opt/module/storm1.2.4/bin/storm ui > /opt/module/storm1.2.4/output.log 2>&1 &nohup /opt/module/storm1.2.4/bin/storm logviewer > /opt/module/storm1.2.4/output.log 2>&1 &\n另外两个节点启动:\nnohup /opt/module/storm1.2.4/bin/storm supervisor > /opt/module/storm1.2.4/output.log 2>&1 &\ncluster100查看进程,出现core nimbus logviewer即成功。\n.svwhxegcygih{zoom:67%;}\n\n另外两个节点出现supervisor\n.xvscbczuluin{zoom:67%;}\n\n9. Cassandra安装\n下载版本为cassandra 3.11.10的版本。完成后解压到/opt/module并修改名为:cassandra3.11.10\n9.1 配置\n在cassandra目录下新建data commitlog saved_caches三个目录。\n修改conf/cassandra.yaml文件。\ndata_file_directories: - /opt/module/cassandra3.11.10/datacommitlog_directory: /opt/module/cassandra3.11.10/commitlogsaved_caches_directory: /opt/module/cassandra3.11.10/saved_caches\n9.2 启动\n执行命令bin/cassandra启动。然后再开一个终端,执行命令bin/cqlsh。显示以下内容:\n.lwrriexbhhqu{zoom:67%;}\n\n启动成功。\n9.3 添加开机自启\nvim /usr/lib/systemd/system/cassandra.service创建服务文件。\n[Unit]Description=Cassandra Server ServiceAfter=network.service [Service]Type=simpleEnvironment=JAVA_HOME=/opt/module/jdk1.8 PIDFile=/usr/local/cassandra/cassandra.pid# 新建一个用户和用户组,Cassandra无法使用root账号启动User=lishanGroup=lishan# 此处为Cassandra包解压后的路径ExecStart=/opt/module/cassandra3.11.10/bin/cassandra -f -p /usr/local/cassandra/cassandra.pidStandardOutput=journalStandardError=journalLimitNOFILE=100000LimitMEMLOCK=infinityLimitNPROC=32768LimitAS=infinity [Install]WantedBy=multi-user.target\n然后刷新服务,设置开机自启\nsudo systemctl daemon-reloadsudo systemctl start cassandrasudo systemctl enable cassandra\n10. 各框架简单操作\n10.1 hadoop\nhadoop基础操作查看hdfs中的文件。有两种方式。\n\n通过UI页面查看\n访问192.168.10.100:9870/explorer.html\n.ccvsmmxpcwyy{zoom:50%;}\n\n可以查看到目前hdfs中存储的文件。\n通过命令查看。\n使用hadoop fs -ls查看目录。\n\n如图,查看根目录下有哪些目录。\n\n10.2 Zookeeper使用\n使用zookeeper目录下的bin/zkServer.sh status命令可以查看到当前节点的状态。\n\n10.3 HBase使用\n通过访问Web页面,查看信息。\n.mbpwolqzcfcx{zoom: 50%;}\n\n10.4 Hive使用\n执行命令hive进入hive cli。\n创建数据库后查询。\n\n然后访问hadoop的web页面。查看hdfs的存储数据\n\n可以在hdfs中找到创建的test数据库。\n10.5 Spark使用\n进入spark目录,执行如下命令,可以使用spark默认的应用测试spark环境。\nbin/spark-submit \\--class org.apache.spark.examples.SparkPi \\--master yarn \\--deploy-mode client \\./examples/jars/spark-examples_2.12-3.0.0.jar \\10 \n运行后,可以进入yarn的页面192.168.10.101:8088,查看到当前正在运行的任务。\n.dbdtmeuebhck{zoom:50%;}\n\n点击任务后的history可以进入到历史页面,查看到任务的历史运行日志。\n.ibhhrxcyroqn{zoom:50%;}\n\n10.6 Storm使用\n可以访问ui页面192.168.10.100:8888查看信息。\n.yjoigbduuwlj{zoom:50%;}\n\n可以看到三个节点都正常运行。\n11. 总结\n本次实验学习了大数据集群环境的搭建,了解到了很多大数据相关的组件。和同组的同学一起学习了相关的知识,我们每个人都搭建了一遍大数据的环境,提高了动手能力。\n附录\n这次实验中,为了方便集群的操作,写了一些脚本,方便后续使用。(基于尚硅谷hadoop教程)\nmyhadoop: 用于一次性启动和关闭所有相关组件\n使用样例myhadoop start myhadoop stop\n#!/bin/bash if [ $# -lt 1 ] then echo \"No Args Input...\" exit ; fi case $1 in \"start\") echo \" =================== 启动 hadoop集群 ===================\" echo \" --------------- 启动 hdfs ---------------\" ssh cluster100 \"/opt/module/hadoop3.1.3/sbin/start-dfs.sh\" echo \" --------------- 启动 yarn ---------------\" ssh cluster101 \"/opt/module/hadoop3.1.3/sbin/start-yarn.sh\" echo \" --------------- 启动 historyserver ---------------\" ssh cluster100 \"/opt/module/hadoop3.1.3/bin/mapred --daemon start historyserver\" echo \"-------------zookeeper--------------\" ssh cluster100 \"/opt/module/zookeeper3.5.7/bin/zkServer.sh start\" ssh cluster101 \"/opt/module/zookeeper3.5.7/bin/zkServer.sh start\" ssh cluster102 \"/opt/module/zookeeper3.5.7/bin/zkServer.sh start\" echo \"----------hbase ----------------\" ssh cluster100 \"/opt/module/hbase2.4.11/bin/start-hbase.sh\" echo \"--------------hiveserver2 metastore-----------\" ssh cluster100 \"nohup /opt/module/hive3.1.3/bin/hive --service hiveserver2 > /opt/module/hive3.1.3/output.log 2>&1 &\" ssh cluster100 \"nohup /opt/module/hive3.1.3/bin/hive --service metastore > /opt/module/hive3.1.3/output.log 2>&1 &\" echo \"--------spark 历史服务器--------------\" ssh cluster100 \"/opt/module/spark3.0.0/sbin/start-history-server.sh\" echo \"--------kafka-------------\" ssh cluster100 \"/opt/module/kafka3.0.0/bin/kafka-server-start.sh -daemon /opt/module/kafka3.0.0/config/server.properties\" ssh cluster101 \"/opt/module/kafka3.0.0/bin/kafka-server-start.sh -daemon /opt/module/kafka3.0.0/config/server.properties\" ssh cluster102 \"/opt/module/kafka3.0.0/bin/kafka-server-start.sh -daemon /opt/module/kafka3.0.0/config/server.properties\" echo \"-----------storm------------\" ssh cluster100 \"nohup /opt/module/storm1.2.4/bin/storm nimbus > /opt/module/storm1.2.4/output.log 2>&1 &\" ssh cluster100 \"nohup /opt/module/storm1.2.4/bin/storm ui > /opt/module/storm1.2.4/output.log 2>&1 &\" ssh cluster100 \"nohup /opt/module/storm1.2.4/bin/storm logviewer > /opt/module/storm1.2.4/output.log 2>&1 &\" ssh cluster101 \"nohup /opt/module/storm1.2.4/bin/storm supervisor > /opt/module/storm1.2.4/output.log 2>&1 &\" ssh cluster102 \"nohup /opt/module/storm1.2.4/bin/storm supervisor > /opt/module/storm1.2.4/output.log 2>&1 &\" ssh cluster101 \"nohup /opt/module/storm1.2.4/bin/storm logviewer > /opt/module/storm1.2.4/output.log 2>&1 &\" ssh cluster102 \"nohup /opt/module/storm1.2.4/bin/storm logviewer > /opt/module/storm1.2.4/output.log 2>&1 &\";; \"stop\") echo \" =================== 关闭 hadoop集群 ===================\" ssh cluster100 \"jps | grep -E 'core|nimbus|logviewer' | awk '{print \\$1}' | xargs kill -9\" ssh cluster101 \"jps | grep -E 'Supervisor|logviewer' | awk '{print \\$1}' | xargs kill -9\" ssh cluster102 \"jps | grep -E 'Supervisor|logviewer' | awk '{print \\$1}' | xargs kill -9\" ssh cluster100 \"/opt/module/kafka3.0.0/bin/kafka-server-stop.sh\" ssh cluster101 \"/opt/module/kafka3.0.0/bin/kafka-server-stop.sh\" ssh cluster102 \"/opt/module/kafka3.0.0/bin/kafka-server-stop.sh\" ssh cluster100 \"/opt/module/spark3.0.0/sbin/stop-history-server.sh\" ssh cluster100 \"jps | grep RunJar | awk '{print \\$1}' | xargs kill -9\" \t\tssh cluster100 \"/opt/module/hbase2.4.11/bin/stop-hbase.sh\" \t\tssh cluster100 \"/opt/module/zookeeper3.5.7/bin/zkServer.sh stop\" \t\tssh cluster101 \"/opt/module/zookeeper3.5.7/bin/zkServer.sh stop\" \t\tssh cluster102 \"/opt/module/zookeeper3.5.7/bin/zkServer.sh stop\" echo \" --------------- 关闭 historyserver ---------------\" ssh cluster100 \"/opt/module/hadoop3.1.3/bin/mapred --daemon stop historyserver\" echo \" --------------- 关闭 yarn ---------------\" ssh cluster101 \"/opt/module/hadoop3.1.3/sbin/stop-yarn.sh\" echo \" --------------- 关闭 hdfs ---------------\" ssh cluster100 \"/opt/module/hadoop3.1.3/sbin/stop-dfs.sh\" ;; *) echo \"Input Args Error...\" ;; esac \nxsync: 用于同步目录或文件到所有节点。\n使用样例 xsync /opt/module/spark\n#!/bin/bash #1. 判断参数个数 if [ $# -lt 1 ]then echo Not Enough Arguement! exit;fi#2. 遍历集群所有机器 for host in cluster100 cluster101 cluster102do echo ==================== $host ==================== #3. 遍历所有目录发送 for file in $@ do #4. 判断文件是否存在 if [ -e $file ] then #5. 获取父目录 pdir=$(cd -P $(dirname $file); pwd) #6. 获取当前文件的名称 fname=$(basename $file) ssh $host \"mkdir -p $pdir\" rsync -av $pdir/$fname $host:$pdir else echo $file does not exists! fi donedone\njpsall: 查看所有节点的进程,并展示出来。\nfor host in cluster100 cluster101 cluster102do echo =============== $host =============== ssh $host jpsdone\n","categories":["大数据"],"tags":["Hive","大数据","Hadoop","Spark"]},{"title":"左神算法笔记","url":"/2024/09/23/%E5%B7%A6%E7%A5%9E%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0/%E5%B7%A6%E7%A5%9E%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0/","content":"\n1 时间复杂度和排序算法\n1.1 交换两个数的写法\npublic void static 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]; }\n异或操作:\n\n相同为0,不同为1\na ^ 0 = a\n\n满足交换律和结合律。对以上交换方法做解释:\n\n第一行:\n\narr[i] = arr[i] ^ arr[j];\narr[j] = arr[j];\n\n第二行:\n\narr[i] = arr[i] ^ arr[j];\narr[j] = arr[i] ^ arr[j] ^ arr[j] = arr[i];\n\n第三行\n\narr[i] = arr[i] ^ arr[j] ^ arr[j] = arr[i] ^ arr[j] ^ arr[i] = arr[j];\narr[j] = arr[i];\n\n\n交换成功。使用以上方法交换的前提是,两个变量指向的内存不能是同一片区域。也就是说对数组进行交换时,交换的下标必须保证不相等。\n1.2 找到数组中出现奇数次的数字\n1.2.1 假设只有一种数出现奇数次\n根据异或操作的结果,如果数字出现偶数次异或完之后会变成0,奇数次的还是原来本身,最后将0和奇数次的数异或还是它本身。\na ^ b ^ c ^ a ^ b ^ b ^ a ^ a ^ c = b;\n因此只有一种数的可以直接遍历所有数然后异或,最终结果就是答案。\n1.2.2 假设有两种数出现奇数次\n首先根据异或操作结果,遍历一遍后,所有偶数次的数都会抵消,只剩下两个奇数次的数的异或记为eor = a ^ b,这里eor不为0,因为a和b是两种数,肯定不相等,所以eor肯定不是0,所以可以在eor中找到至少一个二进制位为1,假设这个二进制位为第i位,那么在第i位中,a和b的值一定不相同。可以根据这个第i位的不同将所有数划分为第i位是1的和0的。\n\n\n\n第i位是0\n第i位是1\n\n\n\n\nothers 0\nothers 1\n\n\na(或b)\nb(或a)\n\n\n\n取另一个变量eorp遍历所有第i位是0或者1的,即可划分出a或者b。这里可以知道eorp的值肯定为a或者b。因为others 0或者others 1一定是偶数个,最终会抵消。因此最后将eor ^ eorp可以得到另一个奇数次的数。\npublic static void printOddTwiceNum(int[] arr) { int eor = 0; for (int cur : arr) { eor ^= cur; } int rightOne = eor & (~eor + 1); int eorp = 0; for (int cur : arr) { if ((cur & rightOne) == 0) { eorp ^= cur; } } // a = eorp, b = (eor ^ eorp)}\n上述代码中int rightOne = eor & (~eor + 1),这行代码作用是获取到从右往左第一个不为0的位置上的数,并且让其他位都为0。其中~eor + 1是eor的补码。假设eor = 10001010则~eor + 1 = 01110110异或后= 00000010。需要注意的是if ((cur & rightOne) == 0)这里的条件只能是== 0 \\ != 0 \\ == rightOne \\ != rightOne。因为rightOne其他位都是0,只有某一位是1,所以异或后的结果,只有这一位是0或者1。\n1.3 插入排序\n插入排序思想:假设0 - i位置的数有序,从i + 1位置向前看,将i + 1位置的数插入到前边有序的数组中使0 - i + 1的数仍然有序。将i + 1的数依次与前一个数比较,如果小于前一个数就交换,然后再与前一个数比较,直到不小于前一个数。\npublic static void insertionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } for (int i = 1; i < arr.length; i++) { for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } }}public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;}\n这里的代码中将比较大小的条件放在了for循环中可以简化代码,同时要注意向前比较时下标不能越界。j指向的前一个数,j + 1指向当前的数,如果条件成立,即前一个数比当前的数大,就交换,交换后j--,然后下一次比较后j指向的仍然是前一个数,j + 1指向的仍然是当前的数。\n1.4 Master公式\n一个递归方法可以表示成如下的递推式:当时,当时,当时,\n1.5 归并排序\n在数组中,每一个数的左边比当前数小的数累加起来,叫做这个数组的小和。例如[1, 3, 2, 4],1左边没有比当前数小的;3左边有,是1;2左边有,是1;4左边有,是1\n3\n2,将这些所有加起来,即为小和。解题思路可以利用归并排序,在合并过程中求小和。归并排序代码:\npublic static void mergeSort(int[] arr) { if (arr == null || arr.length < 2) { return; } mergeSort(arr, 0, arr.length - 1);}public static void mergeSort(int[] arr, int left, int right) { if (left >= right) { return; } // 新建临时数组 int[] t = new int[right - left + 1]; int mid = left + (right - left) / 2; // 对[left, mid]和[mid + 1, right]部分排序 mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); // 将两个排序好的部分合并成一个部分 int i = left, j = mid + 1, k = 0; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) { t[k++] = arr[i++]; } else { t[k++] = arr[j++]; } } // i和j只有一个满足 while (i <= mid) { t[k++] = arr[i++]; } while (j <= right) { t[k++] = arr[j++]; } // 将排序好的部分修改回原始数组 for (i = left, k = 0; i <= right; i++, k++) { arr[i] = t[k]; }}\n同时需要转变一下思路,如果这个数的右边有比当前数大的数,那么当前数就会被加起来。例如left = [1, 2, 2, 4]和right = [2, 3, 5]合并时,left[0] < right[0],可以肯定,right数组中所有的数都比left[0]大,那么right这些数计算时都会加上left[0],因此left[0]会被计算right.length - 0次。而当合并到left[1] = right[0]时,和普通归并排序的合并过程略有不同,这里需要首先合并right数组,因为如果首先合并left数组,想要找到right数组中比left[1]大的数不太容易了,而首先合并right数组的话,可以让right的下标跳出相等的限制,当right的数比left[1]大,就可以找到有多少数比left[1]大,就可以求出小和。\npublic int smallSum(int[] arr) { if (arr == null || arr.length < 2) { return -1; } return mergeSort(arr, 0, arr.length - 1);}private int mergeSort(int[] arr, int left, int right) { if (left >= right) { return 0; } int mid = left + (right - left) / 2; int ret = mergeSort(arr, left, mid) + mergeSort(arr, mid + 1, right); int i = left, j = mid + 1; int k = 0; int[] t = new int[right - left + 1]; while (i <= mid && j <= right) { // 这里和归并排序过程不同 if (arr[i] < arr[j]) { ret += arr[i] * (right - j + 1); t[k++] = arr[i++]; } else { t[k++] = arr[j++]; } } while (i <= mid) { t[k++] = arr[i++]; } while (j <= right) { t[k++] = arr[j++]; } for (i = left, k = 0; i <= right; i++, k++) { arr[i] = t[k]; } return ret;}\n力扣逆序对问题。同样利用归并排序的过程,在归并过程中判断逆序对的数量。\npublic static int reversePairs(int[] arr) { if (arr == null || arr.length < 2) { return 0; } return reversePairs(arr, 0, arr.length - 1);}public static int reversePairs(int[] arr, int left, int right) { if (left >= right) { return 0; } int mid = left + (right - left) / 2; int ret = reversePairs(arr, left, mid) + reversePairs(arr, mid + 1, right); int[] t = new int[right - left + 1]; int i = left, j = mid + 1, k = 0; while (i <= mid && j <= right) { // 如果arr[i] > arr[j]表明,i位置到mid位置的数都比j位置的数大,因为两个部分都是升序排序的。 if (arr[i] <= arr[j]) { t[k++] = arr[i++]; } else { t[k++] = arr[j++]; ret += mid - i + 1; } } while (i <= mid) { t[k++] = arr[i++]; } while (j <= right) { t[k++] = arr[j++]; } for (i = left, k = 0; i <= right; i++, k++) { arr[i] = t[k]; } return ret;}\n1.6 快速排序\n这里采用三路排序,选取一个分界值x,然后划分出小于x的,等于x的,大于x的。然后分别对小于x的和大于x的排序。\npublic static void quickSort(int[] arr, int left, int right) { if (left >= right) { return; } // 随机选取一个分界值,可以有效减少最差情况 int x = arr[left + (int) (Math.random() * (right - left + 1))]; // 小于x的区间的最右端 int less = left - 1; // less + 1到i - 1区间为等于x的区间,同时i会一直遍历待定的数据 int i = left; // 大于x的区间的最左端 int more = right + 1; while (i < more) { // 小于x,将i位置的数和小于x的区间的右端的数交换,并让区间向右扩,i加一,一步完成就是先右扩再交换。 if (arr[i] < x) { swap(arr, i++, ++less); // 大于x,将i位置和大于x的区间的左端的数交换,然后让区间左扩,一步完成就是先左扩再交换。这里不让i加一是因为交换后,不知道交换过来的数是大于x等于x还是小于x,需要判断。 } else if (arr[i] > x) { swap(arr, i, --more); // 等于x的,直接i加一。 } else { i++; } } // 遍历一遍后,less及左边表示小于x的,more及右边表示大于x的 quickSort(arr, left, less); quickSort(arr, more, right);}private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;}\nclass Solution {public: vector<int> sortArray(vector<int>& nums) { quick_sort(nums, 0, nums.size() - 1); return nums; } // cpp 11随机数数引擎,需要引入random头文件。 // 随机数范围:0到int最大值,每次启动程序调用生成的随机数序列相同, // 如果需要每次启动生成的不同可以设置当前时间为随机种子 default_random_engine e; void quick_sort(vector<int> &arr, int left, int right) { if (left >= right) { return; } int x = arr[e() % (right - left + 1) + left]; int less = left - 1, more = right + 1, i = left; while (i < more) { if (arr[i] < x) { swap(arr[++less], arr[i++]); } else if (arr[i] > x) { swap(arr[--more], arr[i]); } else { i++; } } quick_sort(arr, left, less); quick_sort(arr, more, right); }};\n1.7 堆\n大根堆示例如下:\n每个根节点的值都要比孩子节点的值大,如果实行小根堆,则根节点的值要小于孩子节点的值。\n以大根堆为例,主要实现两种操作:\n\n插入数据,在最后插入数据,然后依次向上操作,比较当前数和父节点的大小,如果大于父节点,则进行交换,然后父节点再和它的父节点比较。如下图,插入节点10,该系节点和父节点5比较,然后交换,最后和根节点9比较,然后交换。\n\n\n\n1\n\n\n删除数据,如删除根节点的数据,可以将最后一个数覆盖掉根节点的数,然后向下操作。找到两个孩子(如果有)的最大值,和父节点进行比较,如果大于父节点,则交换,然后再从这个节点向下操作。基于上一个图,将根节点10删除后的状态应该是最后一个值5替换根节点,最后一个值5会被丢掉,这里用涂黑表示。然后从根节点索引0开始向下堆化操作。这里首先和最大的孩子节点9比较,然后交换。\n\n代码实现:\n// 向上操作,堆插入,其中index表示插入的值的索引。public static void heapInsert(int[] arr, int index) { // 如果当前节点大于父节点,则进行交换,然后将索引更新为父节点索引。 // 如果一直向上更新到根节点索引为0,0的父节点的索引计算完后还是0,堆插入终止 while (arr[index] > arr[(index - 1) / 2]) { swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; }}// 向下操作,堆化,其中index表示需要操作的索引,heapSize表示当前堆的大小。public static void heapify(int[] arr, int index, int heapSize) { // 首先获取左孩子的索引 int left = 2 * index + 1; // 如果有左孩子,这也表示有孩子。 while (left < heapSize) { // left + 1表示右孩子,如果右孩子存在,并且值大于左孩子,两个孩子最大值的索引就是右孩子。 // 如果右孩子不存在,或者右孩子值小于左孩子,那么最大值索引就是左孩子。 int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; // 比较孩子中的最大值和父节点,如果孩子值大于父节点的值,则赋值。 largest = arr[largest] > arr[index] ? largest : index; // 如果最大值就是父节点自己本身,则说明满足大根堆,不操作。 if (index == largest) { break; } swap(arr, index, largest); // 继续向下操作。 index = largest; left = 2 * index + 1; }}public static void heapify(int[] arr, int index, int heapSize) { int t = index, left = 2 * index + 1, right = 2 * index + 2; if (left < heapSize && arr[left] > arr[t]) { t = left; } if (right < heapSize && arr[right] > arr[t]) { t = right; } if (t != index) { swap(arr, index, t); heapify(arr, t, heapSize); } }\n如果要删除堆中的第一个元素也就是根节点,可以直接使用heapify方法,index设为0即可,但这个方法也支持从其他位置向下堆化操作。如果想要删除任意位置的元素:\n\n将末尾的值覆盖掉要删除的元素的值。\n基于当前位置进行向上或向下操作。\n\n如果覆盖的值比原值大,则向上操作。\n否则向下操作。\n\n\n但是可以不必进行判断操作,因为在向上向下操作时,会首先判断是否满足条件,因此可以直接执行一次向上和向下操作即可。\n堆的应用\n1.8 堆排序扩展题\n首先给前k+1个数放入小根堆中,那么小根堆的堆顶必然是所有的最小值。因为题目中说移动距离不超过k,那么最小值所在的位置最远是在索引k的位置。取出最小值放入数组,然后往后加入一个元素,选出第二个小的元素,依此类推。具体实现中可以手写堆,也可以使用优先队列。\npublic static void sortedLessK(int[] arr, int k) { PriorityQueue<Integer> heap = new PriorityQueue<>(); int index = 0; for (; index <= Math.min(arr.length, k); index++) { heap.add(arr[index]); } int i = 0; for (; index < arr.length; index++, i++) { arr[i] = heap.poll(); heap.add(arr[index]); } while (!heap.isEmpty()) { arr[i++] = heap.poll(); }}\n1.9 基数排序\n待更新...\n2 链表\n2.1 回文链表的判断\n回文链表\n方法一\n使用栈数据结构,遍历链表压栈,再依次出栈和原链表对比。空间复杂度较高O(N)\n方法二\n快慢指针,快指针走到结尾时,慢指针走到中间,慢指针继续向下遍历,遍历过程反转后边的链表,最后从开始和结尾依次向中间遍历,比对。空间复杂度O(1)\n/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public boolean isPalindrome(ListNode head) { if (head == null) { return true; } ListNode fast = head, slow = head; while (fast.next != null && fast.next.next != null) { fast = fast.next.next; slow = slow.next; } // 如果是奇数,快指针指向最后一个节点 // 如果是偶数,快指针指向倒数第二个节点 // slow再往下移动到右半部分的第一个节点。 // 如果链表长度为偶数,移动后则正好在右半部分第一个; // 如果为奇数,那么可以认为中间的数不属于任何部分,他对整体的回文性没有影响, // 例如1 2 3 2 1,可以认为1 2 为左部分,2 1 为右部分,slow在右部分第一个节点2上 slow = slow.next; // 保存反转后的链表头节点,为后续恢复链表做准备 ListNode tempSlow = reverse(slow); fast = tempSlow; slow = head; while (fast != null) { if (slow.val != fast.val) { return false; } fast = fast.next; slow = slow.next; } // 恢复被反转的链表 reverse(tempSlow); return true; } public ListNode reverse(ListNode head) { ListNode pre = null, cur = head; while (cur != null) { ListNode next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre; }}\n2.2 随机链表的复制\nLCR\n154. 复杂链表的复制 - 力扣(LeetCode)\n方法一\n使用哈希表,保存旧节点和新节点的键值对,然后遍历链表,将节点的下一个节点和随机节点复制。空间复杂度O(N)\npublic Node copyRandomList(Node head) { if (head == null) { return null; } Map<Node, Node> hash = new HashMap<>(); Node temp = head; while (temp != null) { Node copy = new Node(temp.val); hash.put(temp, copy); temp = temp.next; } temp = head; while (temp != null) { hash.get(temp).next = hash.get(temp.next); hash.get(temp).random = hash.get(temp.random); temp = temp.next; } return hash.get(head);}\n方法二\n首先遍历链表,遍历过程中复制当前节点,然后将复制的节点加入到链表中的当前节点后,直到复制完所有节点。复制完后,新复制的节点只有next指针有值,而random指针没有值,下一步在这个新链表上操作,将复制的节点的random连接到正确的节点上。因为每一个旧节点a的下一个一定是复制的节点a_copy,所以旧节点a的random指针指向的节点a_random的下一个节点a_random_copy(即复制的节点)就是a_copy的random指针应该指向的节点。如图蓝色的箭头即为复制的节点的random指针的正确指向。实际中需要注意:random指针可能指向null,此时需要进行判空操作。最后遍历链表,将新复制的链表拆分出来,然后组成新的链表即可,同时原链表也要恢复原状。\n/*// Definition for a Node.class Node { int val; Node next; Node random; public Node(int val) { this.val = val; this.next = null; this.random = null; }}*/class Solution { public Node copyRandomList(Node head) { if (head == null) { return null; } Node temp = head; while (temp != null) { Node copy = new Node(temp.val); copy.next = temp.next; temp.next = copy; temp = temp.next.next; } temp = head; while (temp != null) { Node copy = temp.next; copy.random = temp.random == null ? null : temp.random.next; temp = temp.next.next; } temp = head; Node headcopy = head.next; while (temp != null) { Node copy = temp.next; temp.next = copy.next; temp = temp.next; copy.next = temp == null ? null : temp.next; } return headcopy; }}\n2.3\n判断一个链表是否有环,以及返回入环节点\n环形链表I环形链表II\n方法一\n使用哈希表,遍历整个链表,依次存入链表,存入之前判断是否已经存在,如果存在则当前链表为入环的第一个节点。\npublic ListNode detectCycle(ListNode head) { if (head == null) { return null; } Set<ListNode> set = new HashSet<>(); ListNode temp = head; while (temp != null && !set.contains(temp)) { set.add(temp); temp = temp.next; } return temp;}\n方法二\n快慢指针,快指针一次走两步,慢指针一次走一步,如果快指针遇到null则无环,否则当快慢指针相遇时有环,此时让快指针从头节点开始一次一步,慢指针一次一步,当快慢指针再次相遇时,该节点为入环的第一个节点。\n/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */public class Solution { public boolean hasCycle(ListNode head) { if (head == null) { return false; } ListNode fast = head, slow = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (fast == slow) { return true; } } return false; } // pos表示fast和slow相遇的位置 public ListNode first(ListNode head, ListNode pos) { while (head != pos) { head = head.next; pos = pos.next; } return head; }}\nif (head == null) { return null;}ListNode fast = head, slow = head;while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; // 当快慢指针相同时,必然有环,因此使fast指针为头指针,快慢指针同时移动,即可找到入环节点 if (fast == slow) { fast = head; while (fast != slow) { slow = slow.next; fast = fast.next; } return slow; }}// 走到这一步说明快慢指针不相等,并且快指针可以走到头,那说明没环return null;\n2.4 判断两个链表是否相交\n这里认为链表都是单链表\n\n情况1:两个链表都无环双指针,遍历链表A和B,当指针为null时从另一个节点的头节点开始遍历。直到两个指针相等(两个指针相等时,如果为null则表示没有相交节点,否则有相交节点)面试题\n02.07. 链表相交 - 力扣(LeetCode)\n\npublic class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if (headA == null || headB == null) { return null; } ListNode pA = headA, pB = headB; while (pA != pB) { pA = pA == null ? headB : pA.next; pB = pB == null ? headA : pB.next; } return pA; }}\n❗注意:判断是否为null时,使用的是pA当前指针,而不是pA.next,如果使用pA.next来判断,当不相交时,会发生无限循环的情况,pA和pB会一直不相等(也不为null)。所以使用pA当前指针。可以理解为,把最后链表结束时指向的null指针也算作一个节点,然后两个链表不相交时,最后都会指向null节点,那么两个链表就在null节点“相交”了,如下图所示。\n\n情况2:一个有环一个无环(一定不相交)如果相交,则肯定有一个节点有两个next指针,这不满足单链表。所以一定不相交。\n情况3:两个都有环\n不相交\n在非环处相交\n在环处相交\n\n相交只有两种情况找到两个带环链表的入环节点,然后固定一个,遍历另一个,直到能找到一个节点和固定节点相等,则证明相交,否则不相交。\n// circleNode1 circleNode2是两个节点的入环节点// 如果入环节点相同,说明必然是情况1。否则是情况2或者不相交if (circleNode1 == circleNode2) { // 利用circleNode1或者2为末尾节点,利用无环链表求相交节点方式求相交的节点。 return true;}Node temp = circleNode2.next;while(temp != circleNode2) { if(temp == circleNode1) // circleNode1 或者 circleNode2为相交节点都可以 return true; temp = temp.next;}// 不相交return false;\n所以所有情况如下:\npublic static ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode cycleNodeA = hasCycle(headA); ListNode cycleNodeB = hasCycle(headB); if (cycleNodeA == cycleNodeB) { // 说明两个都是无环的,直接判断是否相交 // if (cycleNodeA == null) { // return getIntersectionNodeNoLoop(headA, headB, null); // } // 否则是都有环,入环节点相同,必然相交,以入环节点为终止节点求相交节点 // return getIntersectionNodeNoLoop(headA, headB, cycleNodeA); // 以上代码可以直接合并为下面一行 return getIntersectionNodeNoLoop(headA, headB, cycleNodeA); } // 说明两个入环节点不相等,要么有一个为空,要么都不为空,如果有一个为空,则表示肯定不相交 if (cycleNodeA == null || cycleNodeB == null) { return null; } // 如果环上相交,说明从一个入环节点开始遍历,一定能到达另一个入环节点 ListNode temp = cycleNodeA; while (temp != cycleNodeB) { temp = temp.next; // 转了一圈发现回到原位置了,说明没有相交 if (temp == cycleNodeA) { return null; } } // 相交,任意一个入环节点都可以是相交节点。 return cycleNodeA; // 如果入环节点不同,并且相交,那么肯定有两个相交节点 // return cycleNodeB;}// 判断一个链表是否有环public static ListNode hasCycle(ListNode head) { if (head == null) { return null; } ListNode slow = head, fast = head; while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; if (fast == slow) { fast = head; while (fast != slow) { fast = fast.next; slow = slow.next; } return fast; } } return null;}// 判断两个无环链表是否相交,手动设定终止节点endNodepublic static ListNode getIntersectionNodeNoLoop(ListNode headA, ListNode headB, ListNode endNode) { ListNode pA = headA, pB = headB; while (pA != pB) { pA = pA == endNode ? headB : pA.next; pB = pB == endNode ? headA : pB.next; } return pA;}\n2.5 找到两个有序链表相同的节点\n解题思路:因为两个链表都是有序的,采用双指针。比较当前值,较小的指针向后移动,如果相等了,加入结果集,同时向后移动,然后继续比较,直到一个越界。\n2.6\n把链表按某个值分为小于 等于 大于的部分\n排序链表\n入门做法\n把链表每个节点(不是值,是节点Node)放在数组里,然后使用快排中的partition。\n高级做法\n首先准备6个指针,分别表示:\n\n\n\nsmallHead\n小于部分的头指针\n\n\n\n\nsmallTail\n小于部分的尾指针\n\n\nequalHead\n等于部分的头指针\n\n\nequalTail\n等于部分的尾指针\n\n\nbigHead\n大于部分的头指针\n\n\nbigTail\n大于部分的尾指针\n\n\n\n使用这六个指针分别串联出小于部分、等于部分、大于部分的链表。遍历整个链表,如果遇到小于的节点,加入到小于部分的链表中,也就是利用尾插法插入到以smallHead为头结点的链表的末尾,等于、大于的节点同理。最后将小于部分的尾连接到等于部分的头,等于部分的尾连接到大于部分的头。但是在实现时要注意:可能不存在小于部分或者等于部分等等,要对每个进行判空处理,防止出现空指针异常。\npublic static ListNode listPartition(ListNode head, int pivot) { // 构造三个类分别表示小于部分的链表,等于部分、大于部分的链表 PartitionNode small = new PartitionNode(); PartitionNode equal = new PartitionNode(); PartitionNode big = new PartitionNode(); ListNode temp = head; while (temp != null) { // 需要频繁插入节点到尾节点,因此用一个方法包装,减少代码量。 if (temp.val < pivot) { small.insertTail(temp); } else if (temp.val > pivot) { big.insertTail(temp); } else { equal.insertTail(temp); } temp = temp.next; } // 最终需要判断小于部分和等于部分的空值情况。 // 如果小于部分不为空,然后等于部分不为空,则连接到等于部分,再连接到大于部分,如果等于部分为空,则直接连接到大于部分 // 如果小于部分为空,等于部分不为空,则连接到大于部分,如果等于部分为空,则说明只有大于部分,直接返回大于部分 if (small.head != null) { if (equal.head != null) { small.tail.next = equal.head; equal.tail.next = big.head; } else { small.tail.next = big.head; } return small.head; } else if (equal.head != null) { equal.tail.next = big.head; return equal.head; } return big.head;}// 定义一个类包含头节点和尾节点。// 算法实现时会频繁在尾部插入节点,会改变头尾指针的值,因此在类中定义头尾指针。static class PartitionNode { ListNode head, tail; void insertTail(ListNode node) { if (head == null) { head = tail = node; } else { tail.next = node; tail = node; } }}// 定义节点类static class ListNode { int val; ListNode next; public ListNode(int val) { this.val = val; this.next = null; }}\n链表快速排序\n由此引出对链表的快速排序。\npublic static PartitionNode quickSortList(ListNode head) { if (head == null || head.next == null) { return new PartitionNode(head, head); } // 通过快慢指针找到中间位置的节点作为中间值,防止边缘特殊情况使算法退化 ListNode pivotNode; ListNode slow = head, fast = head; while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; } pivotNode = slow; // 划分成三部分。 PartitionNode small = new PartitionNode(); PartitionNode equal = new PartitionNode(); PartitionNode big = new PartitionNode(); ListNode temp = head; while (temp != null) { if (temp.val < pivotNode.val) { small.insertTail(temp); } else if (temp.val > pivotNode.val) { big.insertTail(temp); } else { equal.insertTail(temp); } temp = temp.next; } // 划分后对每部分的链表尾节点添加null值,使这三部分链表独立。 if (small.head != null) { small.tail.next = null; } if (equal.head != null) { equal.tail.next = null; } if (big.head != null) { big.tail.next = null; } // 递归排序每部分链表 small = quickSortList(small.head); big = quickSortList(big.head); // 连接三部分链表。 if (small.head != null) { if (equal.head != null) { small.tail.next = equal.head; // 该算法中返回的是完整的链表头节点和尾节点,所以需要确保返回的small的tail对象为整个链表的尾节点 // 后续该操作同理,否则会导致数据丢失。 small.tail = equal.tail; equal.tail.next = big.head; } else { small.tail.next = big.head; } small.tail = big.tail == null ? small.tail : big.tail; return small; } else if (equal.head != null) { equal.tail.next = big.head; equal.tail = big.tail == null ? equal.tail : big.tail; return equal; } return big;} class PartitionNode { ListNode head, tail; void insertTail(ListNode node) { if (head == null) { head = tail = node; } else { tail.next = node; tail = node; } } public PartitionNode() { } public PartitionNode(ListNode head, ListNode tail) { this.head = head; this.tail = tail; }} class ListNode { int val; ListNode next; public ListNode(int val) { this.val = val; this.next = null; }}\n链表归并排序\n对于链表的排序,使用归并排序是更好的选择,可以采用更少的代码完成,同时可以使用迭代的方式优化排序,使空间复杂度O(logn)优化为O(1)。这里先使用递归形式的归并排序。\npublic static ListNode mergeSort(ListNode head) { if (head == null || head.next == null) { return head; } // 使用快慢指针找到中点,如果是奇数个,中点是正中间,如果是偶数,中点是中间两个数的左边的 ListNode fast = head, slow = head; while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; } // 保存慢指针向后移动一个的指针,作为右半部分的起点 // 同时让慢指针的next指向null,表示左半部分和右半部分分成了两个链表。 ListNode mid = slow.next; slow.next = null; // 拆分 ListNode left = mergeSort(head); ListNode right = mergeSort(mid); // 左右分别排序后,进行归并,这里使用尾插法,保证稳定性。 ListNode ret = new ListNode(0); ListNode tail = ret; while (left != null && right != null) { if (left.val <= right.val) { tail.next = left; left = left.next; } else { tail.next = right; right = right.next; } tail = tail.next; } // 有一个链表没遍历完,放到最后即可。 tail.next = left == null ? right : left; return ret.next;}\n❗空间复杂度O(1)的待更新\n3 二叉树\n3.1 遍历\n递归、非递归的前中后序遍历要会写。\n3.1.1 前中后序遍历(非递归)\n\n前序遍历\n\n首先设置一个栈结构用于存储。整体遍历流程如下:❗上述过程要注意:加入孩子节点时一定是先加入右孩子,再加入左孩子,因为栈是后进先出的顺序,所以要后加入左孩子。\npublic static void preOrderUnRecur(Tree root) { if (root == null) { return; } Stack<Tree> stack = new Stack<>(); stack.push(root); while (!stack.isEmpty()) { Tree pop = stack.pop(); System.out.print(pop.val + \" \"); if (pop.right != null) { stack.push(pop.right); } if (pop.left != null) { stack.push(pop.left); } } System.out.println();}\n\n后序遍历\n\n前序遍历的顺序是:根左右,后序遍历的顺序是:左右根。假设现在有前序'(前序撇)遍历:根右左,那么将这个前序'遍历的顺序加入到另一个栈中,最后统一输出,根据栈的现先进后出顺序,那么输出的结果是:左右根。这就是后序遍历的做法。❗有一点注意:前序遍历时保证根左右的顺序,加入子孩子时先加入的右孩子,而现在需要根据前序遍历得到前序'遍历即根右左的顺序,因此需要先加入左孩子。代码如下:\npublic static void postOrderUnRecur(Tree root) { if (root == null) { return; } Stack<Tree> stack1 = new Stack<>(); // 该栈用于收集结果,最后统一将节点输出,满足后序遍历结果 Stack<Tree> stack2 = new Stack<>(); stack1.push(root); while (!stack1.isEmpty()) { Tree pop = stack1.pop(); // 暂时不打印,首先加入到结果栈中 stack2.push(pop); // 先加入左孩子,保证在加入结果栈时可以先出 if (pop.left != null) { stack1.push(pop.left); } if (pop.right != null) { stack1.push(pop.right); } } // 统一输出 while (!stack2.isEmpty()) { System.out.print(stack2.pop().val + \" \"); } System.out.println();}\n\n中序遍历\n\n中序遍历顺序:左根右。算法流程:\n\n如果左孩子不为空,压栈,将新压入的节点设置为“当前节点”继续执行a\n如果左孩子为空,弹出栈,打印\n将弹出栈的节点的右孩子设置为“当前节点”,继续执行a\n\npublic static void midOrderUnRecur(Tree root) { if (root == null) { return; } Stack<Tree> stack = new Stack<>(); // 设置一个指针指向当前遍历到哪个节点,也可以复用root指针 Tree temp = root; // temp != null这个条件是必要的 while (!stack.isEmpty() || temp != null) { // 如果当前指针不为null,压栈 while (temp != null) { stack.push(temp); // 将当前指针指向左孩子 temp = temp.left; } // 说明左孩子为空了,出栈 temp = stack.pop(); // 打印 System.out.print(temp.val + \" \"); // 将当前指针指向右孩子,重复上述操作 temp = temp.right; } System.out.println();}\n❗中序遍历和前边的前序后序遍历代码区别较大,增加了一个指针指向当前遍历到了哪个节点,而且while循环条件中的temp != null是必需的,因为当栈空的时候,指针可能指向了根节点的右孩子,此时还没有遍历完,需要这个条件,防止丢失数据。\n3.1.2 层序遍历\npublic static void levelOrder(Tree root) { if (root == null) { return; } Queue<Tree> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { Tree remove = queue.remove(); System.out.print(remove.val + \" \"); if (remove.left != null) { queue.add(remove.left); } if (remove.right != null) { queue.add(remove.right); } } System.out.println();}\n3.2\n求二叉树一层最多的节点个数(每一层的节点个数)\nLeetCode\n102使用一个变量保存当前层的个数,然后一次性弹出当前层所有的节点,同时加入所有下一层的节点,这样可以保证每次弹出的节点都是同一层的。\npublic static int getMaxLevelNodes(Tree root) { Queue<Tree> queue = new LinkedList<>(); queue.add(root); int maxCnt = 0; while (!queue.isEmpty()) { // size即为当前层的节点个数 int size = queue.size(); maxCnt = Math.max(maxCnt, size); // 一次弹出完该层所有的节点 for (int i = 0; i < size; i++) { Tree remove = queue.remove(); if (remove.left != null) { queue.add(remove.left); } if (remove.right != null) { queue.add(remove.right); } } } return maxCnt;}\n3.3 二叉树的最大宽度\n二叉树最大宽度这道题需要加上两个非空节点中间的null节点的数量,可以按照完全二叉树对数进行编号,然后使用最右端的非空节点索引减去最左端非空节点的索引+1即可得到这一层的宽度。\n// 自定义实现Pair类,用于存储节点和对应的索引值static class Pair<T, V> { T key; V value; public Pair(T key, V value) { this.key = key; this.value = value; }}public static int widthOfBinaryTree(Tree root) { if (root == null) { return 0; } Queue<Pair<Tree, Integer>> queue = new LinkedList<>(); // 存储根节点和索引 queue.add(new Pair<>(root, 0)); int maxCnt = 0; while (!queue.isEmpty()) { // 按照求每层节点个数的方法 int size = queue.size(); // 记录每层节点的最小索引和最大索引 int minIndex = Integer.MAX_VALUE, maxIndex = Integer.MIN_VALUE; for (int i = 0; i < size; i++) { Pair<Tree, Integer> pair = queue.remove(); Tree node = pair.key; Integer index = pair.value; minIndex = Math.min(minIndex, index); maxIndex = Math.max(maxIndex, index); // 左孩子索引为当前索引 * 2 + 1 if (node.left != null) { queue.add(new Pair<>(node.left, 2 * index + 1)); } // 右孩子索引为当前索引 * 2 + 2 if (node.right != null) { queue.add(new Pair<>(node.right, 2 * index + 2)); } } maxCnt = Math.max(maxCnt, maxIndex - minIndex + 1); } return maxCnt;}\n3.4 判断是否是完全二叉树\nLeetCode\n958方法1:分两种情况:\n\n对于任一个节点,如果有右孩子但无左孩子,直接返回false\n在条件1不违反的条件下,如果遇到了第一个左右孩子不全的,如只有左孩子或者没有孩子,那么之后的节点必须全部是叶节点。\n\n☆方法2:层序遍历时将为null的节点也加入进去,如果出现null后,后续出现的节点都要是null否则就不是完全二叉树。\npublic boolean isCompleteTree(TreeNode root) { if (root == null) { return true; } Queue<TreeNode> queue = new LinkedList<>(); queue.add(root); // 设置标记为表示是否出现了null节点 boolean flag = false; while (!queue.isEmpty()) { TreeNode node = queue.remove(); if (node == null) { flag = true; } else { // 如果node不为null,但是flag为true,说明之前出现过null节点,就不是完全二叉树了 if (flag) { return false; } queue.add(node.left); queue.add(node.right); } } return true;}\n3.5 判断满二叉树\n满二叉树的特征:\n\n每一层的节点个数=从开始\n按照完全二叉树的索引编号,最后一个节点的编号为:从开始\n\n本题从每一层节点个数判断,从3.2\n求二叉树一层最多的节点个数复用代码。\npublic static boolean isFullBiTree(Tree root) { if (root == null) { return true; } // 记录高度 int height = 0; Queue<Tree> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { int size = queue.size(); // 判断当前层节点个数是否满足条件 if (size != Math.pow(2, height)) { return false; } for (int i = 0; i < size; i++) { Tree node = queue.remove(); if (node.left != null) { queue.add(node.left); } if (node.right != null) { queue.add(node.right); } } height++; } return true;}\n3.6 判断平衡二叉树\nLeetCode\n110通过求两个子树的高度过程中判断是否是平衡的。类似于后序遍历。如果子树都不是平衡的,那么整棵树一定不是平衡的。通过设立标志位返回值判断是否是平衡的。\nclass Solution { public boolean isBalanced(TreeNode root) { return getHeight(root) >= 0; } public int getHeight(TreeNode root) { if (root == null) { return 0; } int leftHeight = getHeight(root.left); int rightHeight = getHeight(root.right); // 如果高度返回-1,说明子树已经是不平衡的了,那么该棵树也是不平衡的。 // 如果高度都不是-1,说明子树平衡,然后判断整棵树,如果整棵树也不平衡那么返回-1 if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) { return -1; } return Math.max(leftHeight, rightHeight) + 1; }}\n3.7 判断是否是二叉搜索树\nLeetCode\n98利用二叉搜索树的定义,中序遍历时判断上一个数是否小于当前的数。\n// 存储中序遍历的上一个遍历到的值,使用Long类型最小值,Integer类型最小值会越界private long pre = Long.MIN_VALUE;public boolean isValidBST(TreeNode root) { if (root == null) { return true; } // 如果左数不满足条件,整棵树肯定不满足,直接退出 boolean left = isValidBST(root.left); if (!left) { return false; } // 如果上一个数不小于当前的,说明也不满足 if (pre >= root.val) { return false; } // 重置pre,方便下次使用 pre = root.val; // 判断右树 return isValidBST(root.right);}\n3.8\n找到二叉树两个节点的最低公共祖先节点\nLeetCode\n236236.\n二叉树的最近公共祖先 -\n力扣(LeetCode)题解遍历节点,当前节点如果为null或者p``q直接返回。否则遍历左右子树。分别获取到左右子树的返回值。\n\n如果左子树返回空,说明左子树肯定不存在p``q,直接返回right因为必然存在在right中。\n如果左子树返回不空右子树返回空,则说明肯定存在在左子树中,返回left。\n如果都不空,说明左右子树都存在分别存在p``q,则当前root为公共祖先,返回当前的root为公共祖先。\n\npublic TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) { return root; } TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); if (left == null) { return right; } if (right == null) { return left; } return root;}\n3.9 二叉搜索树的中序后继\nLeetCode\n053视频讲解:6.\n图_哔哩哔哩_bilibili(二叉树的中序后继),二叉搜索树可看作是特殊情况首先根据中序遍历的顺序:左根右,一个节点的后续节点是右子树的最左端的节点。所以如果要查找的节点有右子树,那么直接遍历到右子树的最左端节点,即为后续节点。如果没有右子树,那么后续节点一定在该节点的祖先节点上。因为以当前节点为根的子树没有右子树,说明当前节点子树遍历完了,后续节点一定在某个祖先节点或者没有后续节点。\n如上图,第一种情况的后续节点是e,第二种后续节点是b,第三种后续节点是a。针对情况2和3即待查找节点p没有右子树的情况下:\n\n如果p在根节点的左子树中,那么根节点可能就是p的后续节点,后续节点也可能是根节点的左子树中。\n如果p在根节点的右子树中,那么根节点就不是p的后续节点\n\n那么怎么判断p在根节点的左子树还是右子树呢?这就用到了二叉搜索树的性质,如果p节点的值比根节点小那么p就在根节点左子树中,否则在右子树。所以算法如下:\n\n首先判断有没有右子树,如果有则直接找到右子树最左端节点返回。如果没有则执行下边操作\n从根节点开始遍历,依次和p比较,判断p在根节点的哪个子树中\n\n如果在左子树中,更新保存答案的变量,继续左子树\n如果在右子树中,不更新,直接遍历右子树\n直接遍历到最后为null\n\n返回结果\n\npublic TreeNode inorderSuccessor(TreeNode root, TreeNode p) { if (root == null) { return null; } if (p.right != null) { TreeNode temp = p.right; while (temp.left != null) { temp = temp.left; } return temp; } TreeNode temp = root; TreeNode ret = null; while (temp != null) { // 如果p的值大于等于当前根节点的值,说明p在右子树中,当前节点肯定不会是后续节点,所以直接遍历右子树 // 如果p的值小于当前根节点的值,说明p在左子树中,根据中序遍历顺序:左根右,当前根节点可能是后续节点, // 因此先保存当前节点为答案,然后继续遍历左子树(左子树中可能存在真正的后续节点) if (temp.val <= p.val) { temp = temp.right; } else { ret = temp; temp = temp.left; } } return ret;}\n3.10 二叉树的序列化与反序列化\nB站视频\n3.11 判断是否是二叉树\n给定n个节点和m条有向边,节点从[0, n - 1]表示,m条边使用(a,\nb)的数对(a指向b)表示为b的父节点是a。判断给定的结果是不是二叉树。🍬思路:将二叉树看成是一个有向图,除根节点外每个节点的入度为1、出度小于等于2,根节点的入度必须为0、出度小于等于2且只有一个根节点,同时保证从根节点出发可以遍历到所有的节点。可能存在的图的情况:\n\n正确 \n出度不满足条件 \n入度不满足条件\n\n树中存在父子节点之间“互为父子”,则可以从出入度判断不满足条件 \n4. 根节点个数不满足条件\n根节点个数大于1 根节点个数为0\n\n5. 从根节点遍历不能遍历到所有节点\n从出入度判断,只有一个入度为0的节点,所以无法从入度为0的个数判断图是否连通,需要使用dfs遍历\n因此根据上述条件总结出算法流程:\nvoid dfs(vector<vector<int>> &g, int t, bool *st) { st[t] = true; for (int i = 0; i < g[t].size(); i++) { if (!st[g[t][i]]) { dfs(g, g[t][i], st); } }}// 判断是否是二叉树bool isBinaryTree(int n, vector<pair<int, int>> &edges) { // 构造图 vector<vector<int>> g(n, vector<int>()); for (int i = 0; i < edges.size(); i++) { g[edges[i].first].push_back(edges[i].second); } // 求出入度 int inDegree[n], outDegree[n]; for (int i = 0; i < n; i++) { inDegree[i] = 0; outDegree[i] = 0; } for (int i = 0; i < n; i++) { for (int j = 0; j < g[i].size(); j++) { outDegree[i]++; inDegree[g[i][j]]++; } } // 判断根节点数量和索引 int rootIndex = -1, rootCnt = 0; for (int i = 0; i < n; i++) { if (inDegree[i] == 0) { rootCnt++; rootIndex = i; } } if (rootCnt != 1) { return false; } // 从根节点遍历是否能遍历到所有节点 bool st[n]; for (int i = 0; i < n; i++) { st[i] = false; } dfs(g, rootIndex, st); for (int i = 0; i < n; i++) { if (!st[i]) { return false; } } // 判断出入度是否满足条件 for (int i = 0; i < n; i++) { if (outDegree[i] > 2) { return false; } if (i == rootIndex) { continue; } if (inDegree[i] != 1) { return false; } } return true;}\npublic class Solution { public boolean isBinaryTree(int n, List<Integer[]> edges) { List<List<Integer>> g = new ArrayList<>(); for (int i = 0; i < n; i++) { g.add(new ArrayList<>()); } for (Integer[] e : edges) { g.get(e[0]).add(e[1]); } int[] inDegree = new int[n]; int[] outDegree = new int[n]; Arrays.fill(inDegree, 0); Arrays.fill(outDegree, 0); for (int i = 0; i < n; i++) { for (int j : g.get(i)) { outDegree[i]++; inDegree[j]++; } } int rootIndex = -1, rootCnt = 0; for (int i = 0; i < n; i++) { if (inDegree[i] == 0) { rootCnt++; rootIndex = i; } } if (rootCnt != 1) { return false; } boolean[] st = new boolean[n]; dfs(g, rootIndex, st); for (int i = 0; i < n; i++) { if (!st[i]) { return false; } } for (int i = 0; i < n; i++) { if (outDegree[i] > 2) { return false; } if (i == rootIndex) { continue; } if (inDegree[i] != 1) { return false; } } return true; } private void dfs(List<List<Integer>> g, int t, boolean[] st) { st[t] = true; for (int j : g.get(t)) { if (!st[j]) { dfs(g, j, st); } } }}\n4 图\n4.1 图的存储\n这种方式,对于很多算法都很方便,难点在于给定一个图结构,首先需要把给定的图结构转换成该图结构。对于某些算法用不到某些数据,如\n入度in和出度out可以不写\npublic class Graph { // 如果知道确切的顶点个数,可以使用数组代替HashMap\tpublic HashMap<Integer, Node> nodes;\tpublic HashSet<Edge> edges;\t\tpublic Graph() {\t\tnodes = new HashMap<>();\t\tedges = new HashSet<>();\t}}// 点结构的描述public class Node {\tpublic int value;\tpublic int in;\tpublic int out;\tpublic ArrayList<Node> nexts;\tpublic ArrayList<Edge> edges;\tpublic Node(int value) {\t\tthis.value = value;\t\tin = 0;\t\tout = 0;\t\tnexts = new ArrayList<>();\t\tedges = new ArrayList<>();\t}}public class Edge {\tpublic int weight;\tpublic Node from;\tpublic Node to;\tpublic Edge(int weight, Node from, Node to) {\t\tthis.weight = weight;\t\tthis.from = from;\t\tthis.to = to;\t}}// matrix 所有的边// N*3 的矩阵// [weight, from节点上面的值,to节点上面的值]// // [ 5 , 0 , 7]// [ 3 , 0, 1]// public static Graph createGraph(int[][] matrix) {\tGraph graph = new Graph();\tfor (int i = 0; i < matrix.length; i++) {\t\t // 拿到每一条边, matrix[i] \t\tint weight = matrix[i][0];\t\tint from = matrix[i][1];\t\tint to = matrix[i][2]; graph.nodes.putIfAbsent(from, new Node(from));\t\t// if (!graph.nodes.containsKey(from)) {\t\t// \tgraph.nodes.put(from, new Node(from));\t\t// } graph.nodes.putIfAbsent(to, new Node(to));\t\t// if (!graph.nodes.containsKey(to)) {\t\t// \tgraph.nodes.put(to, new Node(to));\t\t// }\t\tNode fromNode = graph.nodes.get(from);\t\tNode toNode = graph.nodes.get(to);\t\t// 无向图可以看作是两条边的有向图 // 第一条边 Edge edge = new Edge(weight, fromNode, toNode); fromNode.nextNodes.add(toNode); fromNode.edges.add(edge); fromNode.out++; toNode.in++; graph.edges.add(edge); // 第二条边 edge = new Edge(weight, toNode, fromNode); toNode.nextNodes.add(fromNode); toNode.edges.add(edge); toNode.out++; fromNode.in++; graph.edges.add(edge);\t}\treturn graph;}\n这个版本代码量少,易于理解\n// 存储每个顶点的信息和该顶点所有边的信息。class Node { public List<Edge> edges = new ArrayList<>();}// 存储每条边的信息(也可以用链表表示,用链表Node中的边只需用头节点存储即可)class Edge { int n, w; public Edge(int node, int weight) { n = node; w = weight; }}public Node[] createGraph(int[][] matrix, int n) { Node[] graph = new Node[n]; for (int i = 0; i < n; i++) { graph[i] = new Node(); } for (int i = 0; i < matrix.length; i++) { int weight = matrix[i][0], from = matrix[i][1], to = matrix[i][2]; graph[from].edges.add(new Edge(to, weight)); } return graph;}\nc++版本\n// 定义边数据结构,存储边到达的下一个顶点和权重struct Edge { int n, w; };// 邻接表表示图,可以用两个vector表示,也可以用一个存储vector的数组表示vector<vector<Edge>> g(n);// vector<Edge> g[n];\n4.2 宽度优先遍历\n如下所示的图:\npublic static void bfs(Graph graph) { Queue<Node> queue = new LinkedList<>(); // 用于判断顶点是否访问过 Set<Node> visited = new HashSet<>(); // 随便选一个顶点作为开始顶点 visited.add(graph.nodes.get(0)); queue.add(graph.nodes.get(0)); while (!queue.isEmpty()) { Node node = queue.remove(); System.out.println(node.value); for (Node nextNode : node.nextNodes) { if (!visited.contains(nextNode)) { visited.add(nextNode); queue.add(nextNode); } } }}\n4.3 深度优先遍历\npublic static void dfs(Graph graph) { Node node = graph.nodes.get(0); Set<Node> visited = new HashSet<>(); visited.add(node); recur(node, visited);}public static void recur(Node node, Set<Node> visited) { System.out.println(node.value); for (Node nextNode : node.nextNodes) { if (!visited.contains(nextNode)) { visited.add(nextNode); recur(nextNode, visited); } }}\n4.4 拓扑排序\n课表排序\nclass Solution { public boolean canFinish(int numCourses, int[][] prerequisites) { Node[] g = new Node[numCourses]; for (int i = 0; i < numCourses; i++) { g[i] = new Node(); } int[] in = new int[numCourses]; for (int[] prereq : prerequisites) { g[prereq[1]].edges.add(prereq[0]); in[prereq[0]]++; } int cnt = 0; Queue<Integer> queue = new LinkedList<>(); for (int i = 0; i < in.length; i++) { if (in[i] == 0) { queue.add(i); cnt++; } } while (!queue.isEmpty()) { int node = queue.remove(); for (int n : g[node].edges) { in[n]--; if (in[n] == 0) { queue.add(n); cnt++; } } } return cnt == numCourses; }}// 边只表示下一个顶点即可,因此不需要单独创建边类class Node { List<Integer> edges = new ArrayList<>();}\n// directed graph and no looppublic static List<Node> sortedTopology(Graph graph) { // key 某个节点 value 剩余的入度 HashMap<Node, Integer> inMap = new HashMap<>(); // 只有剩余入度为0的点,才进入这个队列 Queue<Node> zeroInQueue = new LinkedList<>(); for (Node node : graph.nodes.values()) { inMap.put(node, node.in); if (node.in == 0) { zeroInQueue.add(node); } } List<Node> result = new ArrayList<>(); while (!zeroInQueue.isEmpty()) { Node cur = zeroInQueue.poll(); result.add(cur); for (Node next : cur.nexts) { inMap.put(next, inMap.get(next) - 1); if (inMap.get(next) == 0) { zeroInQueue.add(next); } } } return result;}\n4.5 Kruskal算法\n// 存储每个顶点的父顶点public static HashMap<Integer, Integer> parent = new HashMap<>();// 查询顶点x的父顶点,同时在查询过程中修改结构使得提高查询速度public static int find(int x) { if (parent.get(x) != x) { parent.put(x, find(parent.get(x))); } return parent.get(x);}public static void kruskal(Graph graph) { for (Node node : graph.nodes.values()) { parent.put(node.value, node.value); } // 优先队列保证每次取出的是最小值 Queue<Edge> queue = new PriorityQueue<>((o1, o2) -> o1.weight - o2.weight); queue.addAll(graph.edges); while (!queue.isEmpty()) { Edge edge = queue.remove(); int from = find(edge.from.value); int to = find(edge.to.value); // 如果两个顶点的父顶点不同,说明连接起来不会形成环 if (from != to) { parent.put(from, to); System.out.println(edge.from.value + \" \" + edge.to.value + \" \" + edge.weight); } }}\n4.6 Prim算法\npublic static void prim(Graph graph) { Queue<Edge> queue = new PriorityQueue<>(); Set<Node> visited = new HashSet<>(); // for循环用于处理图不连通的情况 for (Node node : graph.nodes.values()) { if (visited.contains(node)) { continue; } // 找一个node顶点开始 visited.add(node); // 加入node的所有边 queue.addAll(node.edges); while (!queue.isEmpty()) { // 选出解锁的边的最小的边判断 Edge edge = queue.remove(); Node to = edge.to; // 如果这个边的另一个顶点没有加入,则加入到结果中,同时把新加入的顶点的所有边(新解锁的边)加入到堆中 if (visited.contains(to)) { continue; } // 如果该顶点是新顶点,则这个边也可以加入到最小生成树的边中 visited.add(to); System.out.println(edge.from.value + \" \" + edge.to.value + \" \" + edge.weight); queue.addAll(to.edges); } }}\n4.7 前缀Trie树\ntrie前缀树\nclass Trie { boolean isEnd = false; Trie[] next = new Trie[26]; public Trie() { } public void insert(String word) { if (search(word)) { return; } char[] words = word.toCharArray(); Trie cur = this; for (char ch : words) { int index = ch - 'a'; if (cur.next[index] == null) { cur.next[index] = new Trie(); } cur = cur.next[index]; } cur.isEnd = true; } public boolean search(String word) { Trie cur = this; char[] words = word.toCharArray(); for (char ch : words) { int index = ch - 'a'; if (cur.next[index] == null) { return false; } cur = cur.next[index]; } return cur.isEnd; } public boolean startsWith(String prefix) { Trie cur = this; char[] words = prefix.toCharArray(); for (char ch : words) { int index = ch - 'a'; if (cur.next[index] == null) { return false; } cur = cur.next[index]; } return true; }}/** * Your Trie object will be instantiated and called as such: * Trie obj = new Trie(); * obj.insert(word); * boolean param_2 = obj.search(word); * boolean param_3 = obj.startsWith(prefix); */\nclass Trie {public: vector<Trie*> next; bool is_end; Trie() : next(26), is_end(false) {} // c++需要手动释放内存,因为next数组中的指针是new出来的,否则会造成内存泄漏 // 虽然算法题是一次运行,基本不会出现内存泄漏,但是在笔试面试中尽可能完善,所以需要手动释放 ~Trie() { for (auto &p : next) { if (p != nullptr) { delete p; } } } void insert(string word) { if (search(word)) { return; } Trie *cur = this; for (auto &ch : word) { int index = ch - 'a'; if (cur->next[index] == nullptr) { // 这里进行了new操作申请了空间,需要在析构函数中释放掉 cur->next[index] = new Trie(); } cur = cur->next[index]; } cur->is_end = true; } bool search(string word) { Trie *cur = this; for (auto &ch : word) { int index = ch - 'a'; if (cur->next[index] == nullptr) { return false; } cur = cur->next[index]; } return cur->is_end; } bool startsWith(string prefix) { Trie *cur = this; for (auto &ch : prefix) { int index = ch - 'a'; if (cur->next[index] == nullptr) { return false; } cur = cur->next[index]; } return true; }};/** * Your Trie object will be instantiated and called as such: * Trie* obj = new Trie(); * obj->insert(word); * bool param_2 = obj->search(word); * bool param_3 = obj->startsWith(prefix); */\npublic class Code01_Trie {\t// 测试链接 : https://leetcode.cn/problems/implement-trie-ii-prefix-tree/\t// 提交Trie类可以直接通过\t// 原来代码是对的,但是既然找到了直接测试的链接,那就直接测吧\t// 这个链接上要求实现的功能和课上讲的完全一样\t// 该前缀树的路用数组实现\tclass Trie {\t\tclass Node {\t\t\tpublic int pass;\t\t\tpublic int end;\t\t\tpublic Node[] nexts;\t\t\tpublic Node() {\t\t\t\tpass = 0;\t\t\t\tend = 0;\t\t\t\tnexts = new Node[26];\t\t\t}\t\t}\t\tprivate Node root;\t\tpublic Trie() {\t\t\troot = new Node();\t\t}\t\tpublic void insert(String word) {\t\t\tif (word == null) {\t\t\t\treturn;\t\t\t}\t\t\tchar[] str = word.toCharArray();\t\t\tNode node = root;\t\t\tnode.pass++;\t\t\tint path = 0;\t\t\tfor (int i = 0; i < str.length; i++) { // 从左往右遍历字符\t\t\t\tpath = str[i] - 'a'; // 由字符,对应成走向哪条路\t\t\t\tif (node.nexts[path] == null) {\t\t\t\t\tnode.nexts[path] = new Node();\t\t\t\t}\t\t\t\tnode = node.nexts[path];\t\t\t\tnode.pass++;\t\t\t}\t\t\tnode.end++;\t\t}\t\tpublic void erase(String word) {\t\t\tif (countWordsEqualTo(word) != 0) {\t\t\t\tchar[] chs = word.toCharArray();\t\t\t\tNode node = root;\t\t\t\tnode.pass--;\t\t\t\tint path = 0;\t\t\t\tfor (int i = 0; i < chs.length; i++) {\t\t\t\t\tpath = chs[i] - 'a';\t\t\t\t\tif (--node.nexts[path].pass == 0) {\t\t\t\t\t\tnode.nexts[path] = null;\t\t\t\t\t\treturn;\t\t\t\t\t}\t\t\t\t\tnode = node.nexts[path];\t\t\t\t}\t\t\t\tnode.end--;\t\t\t}\t\t}\t\tpublic int countWordsEqualTo(String word) {\t\t\tif (word == null) {\t\t\t\treturn 0;\t\t\t}\t\t\tchar[] chs = word.toCharArray();\t\t\tNode node = root;\t\t\tint index = 0;\t\t\tfor (int i = 0; i < chs.length; i++) {\t\t\t\tindex = chs[i] - 'a';\t\t\t\tif (node.nexts[index] == null) {\t\t\t\t\treturn 0;\t\t\t\t}\t\t\t\tnode = node.nexts[index];\t\t\t}\t\t\treturn node.end;\t\t}\t\tpublic int countWordsStartingWith(String pre) {\t\t\tif (pre == null) {\t\t\t\treturn 0;\t\t\t}\t\t\tchar[] chs = pre.toCharArray();\t\t\tNode node = root;\t\t\tint index = 0;\t\t\tfor (int i = 0; i < chs.length; i++) {\t\t\t\tindex = chs[i] - 'a';\t\t\t\tif (node.nexts[index] == null) {\t\t\t\t\treturn 0;\t\t\t\t}\t\t\t\tnode = node.nexts[index];\t\t\t}\t\t\treturn node.pass;\t\t}\t}}\n4.8 迪杰斯特拉算法\n网络延迟时间\n\n普通算法\n\npublic static Map<Node, Integer> dijkstra(Graph graph) { // 保存起始顶点到每个顶点的距离,如果没有则认为距离为正无穷 Map<Node, Integer> distanceMap = new HashMap<>(); // 设置0为起始顶点 Node minNode = graph.nodes.get(0); // 起始顶点到自己的距离为0 distanceMap.put(minNode, 0); // 保存哪些顶点加入到判断完的集合中(距离已经是最短的) Set<Node> visited = new HashSet<>(); while (minNode != null) { Integer distance = distanceMap.get(minNode); for (Edge edge : minNode.edges) { // 如果不存在则表示起始顶点到edge.to顶点距离为正无穷,直接添加 // 如果存在则需要判断 起始顶点直接到edge.to顶点的距离 和 // 起始顶点到minNode顶点加上minNode到edge.to的距离哪个更小 if (!distanceMap.containsKey(edge.to)) { distanceMap.put(edge.to, distance + edge.weight); } else { distanceMap.put(edge.to, Math.min(distance + edge.weight, distanceMap.get(edge.to))); } } // 更新结束,将minNode加入集合中 visited.add(minNode); // minNode置为null,(重要),后续循环退出条件是通过判断minNode是否为空 minNode = null; // 保存最小距离 int minDist = Integer.MAX_VALUE; // 遍历所有的距离,在没有判断确定的顶点中选择距离最短的 for (Map.Entry<Node, Integer> entry : distanceMap.entrySet()) { Node node = entry.getKey(); Integer dist = entry.getValue(); // 如果已经是判断过的顶点则直接跳过 if (!visited.contains(node) && minDist > dist) { minDist = dist; minNode = node; } } } return distanceMap;}\n另一种方式(推荐),图存储方式简单些\nclass Solution { public int networkDelayTime(int[][] times, int n, int k) { Node[] g = new Node[n]; for (int i = 0; i < n; i++) { g[i] = new Node(); } for (int[] time : times) { g[time[0] - 1].edges.add(new Edge(time[1] - 1, time[2])); } int[] dist = new int[n]; Arrays.fill(dist, 0x3f3f3f3f); boolean[] st = new boolean[n]; dist[k - 1] = 0; for (int i = 0; i < n - 1; i++) { int t = -1; for (int j = 0; j < n; j++) { if (!st[j] && (t == -1 || dist[t] > dist[j])) { t = j; } } st[t] = true; for (Edge edge : g[t].edges) { dist[edge.n] = Math.min(dist[edge.n], dist[t] + edge.w); } } int maxDist = Arrays.stream(dist).max().getAsInt(); return maxDist == 0x3f3f3f3f ? -1 : maxDist; }}class Edge { int n, w; public Edge(int node, int weight) { n = node; w = weight; }}class Node { List<Edge> edges = new ArrayList<>();}\nanother cpp版本\nclass Solution {public: struct Edge { int n, w; }; int INF = 0x3f3f3f3f; int networkDelayTime(vector<vector<int>>& times, int n, int k) { // 两个vector表示邻接表 // vector<vector<Edge>> g(n); // 或者vector<Edge>数组表示邻接表 vector<Edge> g[n]; // foreach循环使用引用提高速度 for (auto &time : times) { g[time[0] - 1].push_back({time[1] - 1, time[2]}); } int dist[n]; bool st[n]; memset(dist, 0x3f, sizeof dist); memset(st, 0, sizeof st); dist[k - 1] = 0; for (int i = 0; i < n - 1; i++) { int t = -1; for (int j = 0; j < n; j++) { if (!st[j] && (t == -1 || dist[t] > dist[j])) { t = j; } } st[t] = true; for (auto &edge : g[t]) { dist[edge.n] = min(dist[edge.n], dist[t] + edge.w); } } int maxDist = *max_element(dist, dist + n); return maxDist == INF ? -1 : maxDist; }};\n\n优化算法\n\n5 贪心算法\n5.1 会议安排\nLeetCode\n646\nclass Solution { public int findLongestChain(int[][] pairs) { Arrays.sort(pairs, (a, b) -> a[1] - b[1]); int retLength = 0, curTime = Integer.MIN_VALUE; for (int[] pair : pairs) { if (curTime < pair[0]) { retLength++; curTime = pair[1]; } } return retLength; }}\n\nclass Program { int start, end;}public static int maxMeetings(Program[] programs) { // 按照结束时间排序 Arrays.sort(programs, (p1, p2) -> p1.end - p2.end); int timePoint = programs[0].start; int ret = 0; for (int i = 0; i < programs.length; i++) { if (timePoint <= programs[i].start) { timePoint = programs[i].end; ret++; } } return ret;}\n5.2 字符串数组排序\npublic static void sortString(String[] strs) { // 按照两个字符串不同拼接方式排序 Arrays.sort(strs, (s1, s2) -> (s1 + s2).compareTo(s2 + s1));}\n5.3 金条划分\n\n\n按照上图中的两个不同分割法:\n\n第一种:先划分为10和50,消耗60,再划分为20和30,消耗50,总计110。\n第二种:先划分为30和30,消耗60,再划分为10和20,消耗30,总计90。\n\n如果要取到最小的,需要先按照最大的划分,逆向过来,这是一颗哈夫曼树。\npublic static int lessMoney(int[] arr) { Queue<Integer> heap = new PriorityQueue<>(); for (int a : arr) { heap.add(a); } int cur = 0, sum = 0; while (heap.size() > 1) { cur = heap.remove() + heap.remove(); sum += cur; heap.add(cur); } return sum;}\n5.4 利润最大\nLeetCode\n502\n先按照成本花费升序排序,每一轮选出可以做的(成本不大于当前资金的)项目,加入到另一个集合中保存,选出利润最大的做,然后更新当前可用资金。\nclass Solution { public int findMaximizedCapital(int k, int w, int[] profits, int[] capital) { Queue<Pair> capHeap = new PriorityQueue<>((p1, p2) -> p1.c - p2.c); Queue<Pair> proHeap = new PriorityQueue<>((p1, p2) -> p2.p - p1.p); for (int i = 0; i < profits.length; i++) { capHeap.add(new Pair(profits[i], capital[i])); } for (int i = 0; i < k; i++) { while (!capHeap.isEmpty() && capHeap.peek().c <= w) { proHeap.add(capHeap.remove()); } if (proHeap.isEmpty()) { return w; } w += proHeap.remove().p; } return w; }}class Pair { int p, c; public Pair(int p, int c) { this.p = p; this.c = c; }}\n5.5 数据流的中位数(堆的应用)\nLeetCode\n295\n用两个堆,一个大根堆一个小根堆,其中大根堆保存小于等于中位数部分的数据,小根堆保存大于中位数部分的数据。\n插入数据算法流程:\n\n如果大根堆空,直接加入大根堆,结束,否则执行2\n如果小于等于大根堆堆顶,加入大根堆,否则加入小根堆\n判断大小根堆数据量差值,如果插值大于1,则将数据量多的堆的堆顶弹出加入到另一个堆中\n\n查询算法流程:\n\n如果两个堆数据量相同,则大小根堆查询堆顶相加除以2,得到中位数,结束,否则执行2\n返回数据量大的堆的堆顶元素\n\n\n\n5.5-1.drawio.svg\n\nclass MedianFinder { private Queue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a); private Queue<Integer> minHeap = new PriorityQueue<>(); public MedianFinder() { } public void addNum(int num) { if (maxHeap.isEmpty()) { maxHeap.add(num); return; } if (num <= maxHeap.peek()) { maxHeap.add(num); } else { minHeap.add(num); } if (maxHeap.size() - minHeap.size() > 1) { minHeap.add(maxHeap.remove()); } else if (minHeap.size() - maxHeap.size() > 1) { maxHeap.add(minHeap.remove()); } } public double findMedian() { if (maxHeap.size() == minHeap.size()) { return (maxHeap.peek() + minHeap.peek()) / 2.0; } if (maxHeap.size() > minHeap.size()) { return maxHeap.peek(); } else { return minHeap.peek(); } }}/** * Your MedianFinder object will be instantiated and called as such: * MedianFinder obj = new MedianFinder(); * obj.addNum(num); * double param_2 = obj.findMedian(); */\n6 暴力递归\nLeetCode\n940\n6.1 字符串全排列\nLeetCode\n46\n\n不去重\n去重\n\npublic static List<String> permutation3(String s) { List<String> ans = new ArrayList<>(); if (s == null || s.length() == 0) { return ans; } char[] str = s.toCharArray(); g2(str, 0, ans); return ans;}public static void g2(char[] str, int index, List<String> ans) { if (index == str.length) { ans.add(String.valueOf(str)); } else { boolean[] visited = new boolean[256]; for (int i = index; i < str.length; i++) { // 不去重删除掉这个if判断代码即可 if (!visited[str[i]]) { visited[str[i]] = true; swap(str, index, i); g2(str, index + 1, ans); swap(str, index, i); } } }}\n6.2 预测赢家\nLeetCode\n486\nclass Solution { public boolean predictTheWinner(int[] nums) { return dfs(nums, 0, nums.length - 1) >= 0; } private int dfs(int[] nums, int i, int j) { if (i == j) { return nums[i]; } int left = nums[i] - dfs(nums, i + 1, j); int right = nums[j] - dfs(nums, i, j - 1); return Math.max(left, right); }}\n可以设置两个函数,分别为先手和后手函数。记为f(arr, l, r)和s(arr, l, r)分别表示在arr中的l和r范围内进行决策得到的结果。\nint f(arr, l, r) { // 如果只有一个数,那么先手只能拿这一个 if (l == r) { return arr[l]; } // 否则有两种决策情况,一种是拿最左侧,一种是拿最右侧 // 而拿完后,剩下的数对于当前来说属于后手,所以调用后手函数,而为了保证当前分数为最大值,所以使用max取最大值 return max(arr[l] + s(arr, l + 1, r), arr[r] + s(arr, l, r - 1));}\nint s(arr, l, r) { // 如果为后手,只有一个数,那么先手会拿到这个数,后手没得拿只能返回0 if (l == r) { return 0; } // 后手拿[l, r]范围内的数,只有两种可能,因为先手只能拿最左侧或最右侧。 // 先手拿完后,后手变成了先手,所以调用先手函数。 // 而同时两个人都保证分数最大化,所以先手拿完后肯定要保证后手是分数最小化,所以需要取最小值。 return min(f(arr, l + 1, r), f(arr, l, r - 1));}\n1423.\n可获得的最大点数 - 力扣(LeetCode)\n6.3 解码方法\nLeetCode\n91\n递归方法(会超时)\nclass Solution { public int numDecodings(String s) { return numDecodings(s.toCharArray(), 0); } public int numDecodings(char[] chs, int i) { if (i == chs.length) { return 1; } if (chs[i] == '0') { return 0; } int ret = numDecodings(chs, i + 1); if (chs[i] == '1') { if (i + 1 < chs.length) { ret += numDecodings(chs, i + 2); } } if (chs[i] == '2') { if (i + 1 < chs.length && chs[i + 1] < '7') { ret += numDecodings(chs, i + 2); } } return ret; }}\n6.4 n皇后问题\n常规方法\n// 当前来到i行,一共是0~N-1行// 在i行上放皇后,所有列都尝试// 必须要保证跟之前所有的皇后不打架// int[] record record[x] = y 之前的第x行的皇后,放在了y列上// 返回:不关心i以上发生了什么,i.... 后续有多少合法的方法数public static int process1(int i, int[] record, int n) { if (i == n) { return 1; } int res = 0; // i行的皇后,放哪一列呢?j列, for (int j = 0; j < n; j++) { if (isValid(record, i, j)) { record[i] = j; res += process1(i + 1, record, n); } } return res;}public static boolean isValid(int[] record, int i, int j) { // 0..i-1 for (int k = 0; k < i; k++) { // 判断是不是在同一列或者是在同一斜线上,此处用斜率判断,1或者-1 if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) { return false; } } return true;}\n// 请不要超过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);}// 7皇后问题// limit : 0....0 1 1 1 1 1 1 1// 之前皇后的列影响:colLim// 之前皇后的左下对角线影响:leftDiaLim// 之前皇后的右下对角线影响:rightDiaLimpublic static int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim) { if (colLim == limit) { return 1; } // pos中所有是1的位置,是你可以去尝试皇后的位置 int pos = limit & (~(colLim | leftDiaLim | rightDiaLim)); int mostRightOne = 0; int res = 0; while (pos != 0) { mostRightOne = pos & (~pos + 1); pos = pos - mostRightOne; res += process2(limit, colLim | mostRightOne, (leftDiaLim | mostRightOne) << 1, (rightDiaLim | mostRightOne) >>> 1); } return res;}\n打印所有可能的n皇后解法:\nimport java.util.*;class Main { public static StringBuilder sb = new StringBuilder(); public static char[][] g; public static int[] record; public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); g = new char[n][n]; record = new int[n]; Arrays.fill(record, -1); for (int i = 0; i < n; i++) { Arrays.fill(g[i], '.'); } recur(0, n); sc.close(); } public static void recur(int i, int n) { if (i == n) { // 清空sb内容。 sb.setLength(0); for (int k = 0; k < n; k++) { for (int j = 0; j < n; j++) { sb.append(g[k][j]); } sb.append('\\n'); } System.out.println(sb); } else { for (int j = 0; j < n; j++) { if (isValid(i, j)) { record[i] = j; g[i][j] = 'Q'; recur(i + 1, n); g[i][j] = '.'; record[i] = -1; } } } } public static boolean isValid(int i, int j) { // (i, j) (k, record[k]) for (int k = 0; k < i; k++) { if (j == record[k] || Math.abs(i - k) == Math.abs(j - record[k])) { return false; } } return true; }}\n51. N 皇后 -\n力扣(LeetCode)\nclass Solution { public List<List<String>> solveNQueens(int n) { List<List<String>> ret = new ArrayList<>(); char[][] g = new char[n][n]; StringBuilder sb = new StringBuilder(); int[] record = new int[n]; Arrays.fill(record, -1); for (int i = 0; i < n; i++) { Arrays.fill(g[i], '.'); } dfs(0, n, g, record, ret); return ret; } public void dfs(int i, int n, char[][] g, int[] record, List<List<String>> ret) { if (i == n) { List<String> temp = new ArrayList<>(); for (int j = 0; j < n; j++) { temp.add(String.valueOf(g[j])); } ret.add(temp); return; } for (int j = 0; j < n; j++) { if (isValid(i, j, record)) { record[i] = j; g[i][j] = 'Q'; dfs(i + 1, n, g, record, ret); g[i][j] = '.'; record[i] = -1; } } } public boolean isValid(int i, int j, int[] record) { for (int k = 0; k < i; k++) { if (j == record[k] || Math.abs(i - k) == Math.abs(j - record[k])) { return false; } } return true; }}\n7 哈希函数与哈希表\n哈希函数的性质(简要记录):\n\n无穷多的值映射到有范围的值\n相同的输入得到相同的输出\n不同的输入可能得到相同的输出\n映射后的值分布比较均匀\n\n7.1 例题\n题目:如果有一个40亿条记录的大文件,现需要求出现次数最多的记录,限制内存使用为1GB。解法:如果直接采用哈希表的方式按照(记录,次数)的方式存储每个记录的次数,内存肯定会不够。因此采用哈希函数首先将记录做一次哈希,然后将得到的值模100,最后肯定会得到一个0-99的值,按照模除后的结果将该条记录存到对应的小文件中,再对每个小文件使用哈希表的方式进行统计(此时的内存空间是够用的)。❗如果采用摩尔投票法需要保证重复的记录超过总记录数的一半。\n7.2 布隆过滤器\n主要用于类似黑名单系统,查找当前记录是否存在于“黑名单”中,并且这个黑名单只会添加记录,不会删除记录,使用布隆过滤器可以比使用hash表大大减少空间。但是布隆过滤器存在一定的失误率,即不存在于黑名单的记录可能也会认为是黑名单,可以通过设置大小大大降低失误率,但是失误率是一直存在的。使用到的数据结构:二进制位组成的集合。算法过程:\n\n准备个哈希函数\n添加黑名单\n\n遍历黑名单记录列表\n\n遍历哈希函数\n\n每个哈希函数求得一个哈希值\n将位集合中的位设置为1\n\n\n\n判断是否在黑名单中\n\n对于一个记录遍历哈希函数\n\n求得一个哈希值\n判断是否为1\n\n当所有的位置都为1时说明在黑名单中(可能存在失误),否则不在\n\n\n首先需要构造一个二进制位集合,这里可以采用int数组的形式,每一个值表示32位\n// 设总共需要m位的集合// 上取整int a[ceil(m / 32)];// 将第i位设置为1void setI1(int *a, int i) { int index = i / 32, bit_index = i % 32; a[index] = a[index] | (1 << bit_index);}// 将第i位设置为0void setI0(int *a, int i) { int index = i / 32, bit_index = i % 32; a[index] = a[index] & (~(1 << bit_index))}// 获取第i位的值(0或1)int getI(int *a, int i) { int index = i / 32, bit_index = i % 32; return (a[index] >> bit_index) & 1;}\n🍰位的集合长度和个哈希函数怎么确定和的值呢?可以用三个公式来确定,首先给定样本量(即黑名单数量)和失误率,有:真真真真前两个公式计算出的是理论上的集合长度和理论上的哈希函数个数,实际中集合长度可以更大一些即真,而哈希函数个数需要采用上取整的方式即真,以此再次求得真实的失误率。\n7.3 一致性哈希\n服务器利用hash\nkey进行划分做负载均衡,尽量选取种类多,且分布较均匀的key。否则会导致某些服务器资源利用过多(如某些key使用很多,但另一些使用很少)。如果后期需要增加服务器或者减少,则会导致数据迁移,而这个数据迁移是全量的迁移。为了解决数据迁移的问题,可以将进行hash运算过后的数值范围看作一个环,将服务器分配到hash环上,然后访问或添加时,计算hash值后,判断结果距离哪个服务器的hash值最近(可以统一采用顺时针的方式),这样之后如果要添加服务器m4(假设计算后位于m2和m3中间),数据只需要迁移m2和m4之间的数据到m4中即可。大大减小了迁移量,但是这会引起新的问题:\n\n无法保证初始分配的节点比较少时是均匀分配的(hash计算能保证大数据量的情况下是均分的)\n添加一个节点后也不能保证添加之后是均分的\n\n解决方式:虚拟节点假设m1有1000个(虚拟)节点名称:(a1,\na2, a3, ...,\na1000),m2和m3同理将这3000个虚拟节点计算hash后分布到hash环中,数据量较大,可以保证分配比较均匀。同时如果加入节点,也是按照虚拟节点加入,也能保证加入后的分布较均匀。还有一个好处是,如果m1的性能较好,可以给m1设置较多的虚拟节点,让更多的数据分配到m1,如果m3性能较差可以分配较少的虚拟节点,以进行服务器负载的管理。\n8 有序表、并查集\n岛屿问题\nint getIsland(vector<vector<int>> &m, int N, int M) { int ret = 0; for (int i = 0; i < N; i++) { for (int j = 0; j < M; j++) { if (m[i][j] == 1) { ret++; dfs(m, i, j, N, M); } } } return ret;}void dfs(vector<vector<int>> &m, int i, int j, int N, int M) { if (i < 0 || i >= N || j < 0 || j >= M || m[i][j] == 0) { return; } m[i][j] = 2; dfs(m, i - 1, j, N, M); dfs(m, i, j - 1, N, M); dfs(m, i + 1, j, N, M); dfs(m, i, j + 1, N, M);}\n时间复杂度⭐进阶:利用并行算法解决这个问题视频链接:左神视频链接\n8.1 并查集\npublic class UnionFind { private int[] p; public UnionFind(int n) { // 初始化,设置每个元素的父元素都为自己 p = new int[n]; for (int i = 0; i < n; i++) { p[i] = i; } } // 查找 public int find(int x) { if (x != p[x]) { // 查找同时优化节点,减少树的层数 p[x] = find(p[x]); } return p[x]; } // 合并 public void union(int a, int b) { int pa = find(a); int pb = find(b); if (pa != pb) { p[pa] = pb; } }}\n9 KMP算法\n","categories":["技术"],"tags":["算法"]},{"title":"J2Cache升级踩坑","url":"/2023/10/20/%E8%AF%BE%E7%A8%8B/J2Cache%E5%8D%87%E7%BA%A7%E8%B8%A9%E5%9D%91/","content":"1. 运行项目\n使用命令git clone https://gitee.com/ld/J2Cache将项目克隆下来,具体安装运行方法参见README.md文档。\n\n2. 部分升级\n2.1 Java升级\n项目默认使用的jdk1.8,将项目升级为jdk17。修改jdk版本后,使用maven打包项目会报错程序包javax.annotation不存在。需要在根目录的pom.xml添加依赖引入javax.annotation。\n<dependency> <groupId>javax.annotation</groupId> <artifactId>jsr250-api</artifactId> <version>1.0</version></dependency>\n继续进行下一步,执行命令runtest.bat,会报错Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not \"opens java.lang\" to unnamed module @4ed07f8c。通常在jdk9及以上会遇到该问题。\n通过mvn命令解决该问题暂时没有找到方法。\n可以通过在IDEA中直接运行main方法,添加vm参数,解决该问题。需要添加的vm参数为:\n--add-opens java.base/java.lang=ALL-UNNAMED--add-opens java.base/java.math=ALL-UNNAMED--add-opens java.base/java.util=ALL-UNNAMED--add-opens java.base/java.util.concurrent=ALL-UNNAMED--add-opens java.base/java.net=ALL-UNNAMED--add-opens java.base/java.text=ALL-UNNAMED\n由于main方法中,获取的是System.Console,而在IDEA中直接运行,获取不到该对象,需要修改读入输入的对象,将其修改为Scanner对象进行读入。\n\n2.2 Redis升级\n将Redis升级5.0版本后。\nBinaryJedisCommands类报错,原因是删除了该类,使用JedisBinaryCommands类替代。\n在RedisClient.java类中,\n重写注解报错,父类没有该方法,删除即可。\n\n包装类报错,原因是新版本使用基本类型代替了包装类。\n\n新版本中将返回值由Set类型改为了List。\n\n新版本中移除了类ShardedJedisPool,引入JedisSharding替换。\n\n参数设置错误,只需要一个HostAndPort的列表即可。\n\n最后一个参数poolConfig原来参数类型为JedisPoolConfig,新版本构造函数的类型修改为GenericObjectPoolConfig<Connection>。\n在RedisUtils工具类中,将newPoolConfig方法参数修改为GenericObjectPoolConfig<Jedis>。同时添加方法transferPoolConfig,将GenericObjectPoolConfig<Jedis>转换为GenericObjectPoolConfig<Connection>。\n在类RedisPubSubClusterPolicy类中,找不到Pool类,将这个类的包修改为import redis.clients.jedis.util.Pool;\n在RedisGenericCache类中,\n将报错类更改为以上类。\nRedisPubSubClusterPolicy类中,父类方法指定为final,不能被重写了。\n\n2.3 Rocketmq升级\n升级到5.1.4后报错\n\n2.4 FastJson升级\n\n升级到2.0.41,其中groupId和artifactId也进行了更改。\nFastjsonSerializer类里\n\n需要修改为com.alibaba.fastjson2,同时引入新类。\n","tags":["J2Cache","SpringBoot3"]},{"title":"LeetCode SQL题笔记","url":"/2023/12/01/SQL%E9%A2%98/LeetCode/Sql%E9%A2%98/","content":"197.上升的温度\n表: Weather\n+---------------+---------+| Column Name | Type |+---------------+---------+| id | int || recordDate | date || temperature | int |+---------------+---------+id 是该表具有唯一值的列。该表包含特定日期的温度信息\n\n编写解决方案,找出与之前(昨天的)日期相比温度更高的所有日期的\nid 。\n返回结果 无顺序要求 。\n结果格式如下例子所示。\n示例 1:\n输入:Weather 表:+----+------------+-------------+| id | recordDate | Temperature |+----+------------+-------------+| 1 | 2015-01-01 | 10 || 2 | 2015-01-02 | 25 || 3 | 2015-01-03 | 20 || 4 | 2015-01-04 | 30 |+----+------------+-------------+输出:+----+| id |+----+| 2 || 4 |+----+解释:2015-01-02 的温度比前一天高(10 -> 25)2015-01-04 的温度比前一天高(20 -> 30)\n解析(datediff函数)\n这道题首先是要得到id,其次是需要比较日期和温度,温度容易比较,使用> <等即可,但是日期类型不能直接比较,否则会有问题,如1号的前一天是30号或者31号,因此需要使用日期比较函数,这里使用的是datediff(日期1, 日期2)函数,该函数返回的是日期1 - 日期2相差的天数,如datediff('2023-01-11', '2023-01-10')返回的是1,而datediff('2023-01-10', '2023-01-11')返回的是-1。\n使用什么符号、函数进行比较解决了,接下来需要思考如何解答这道题。需要判断的是“今天”比“昨天”的温度高的,也就是说datediff(今天, 昨天)应该返回1,但是在一个表直接比较两个日期不容易实现,因此可以尝试使用自连接。\nselect w2.id idfrom weather w1, weather w2where datediff(w2.recordDate , w1.recordDate) = 1and w2.temperature > w1.temperature;\n以上sql表示,从左表w1(驱动表)取出所有数据,然后依次用每一条数据对比右表w2(被驱动表)的数据,对比的条件就是datediff(w2.recordDate , w1.recordDate) = 1 and w2.temperature > w1.temperature,当满足这个条件时,即返回“今天”(w2.id)的数据,因为条件是w2日期比较大,如果需要返回“昨天”的数据即返回w1.id。\n586.订单最多的客户\n表: Orders\n+-----------------+----------+| Column Name | Type |+-----------------+----------+| order_number | int || customer_number | int |+-----------------+----------+在 SQL 中,Order_number是该表的主键。此表包含关于订单ID和客户ID的信息。\n查找下了 最多订单 的客户的\ncustomer_number 。\n测试用例生成后, 恰好有一个客户\n比任何其他客户下了更多的订单。\n查询结果格式如下所示。\n示例 1:\n输入: Orders 表:+--------------+-----------------+| order_number | customer_number |+--------------+-----------------+| 1 | 1 || 2 | 2 || 3 | 3 || 4 | 3 |+--------------+-----------------+输出: +-----------------+| customer_number |+-----------------+| 3 |+-----------------+解释: customer_number 为 '3' 的顾客有两个订单,比顾客 '1' 或者 '2' 都要多,因为他们只有一个订单。所以结果是该顾客的 customer_number ,也就是 3 。\n进阶:\n如果有多位顾客订单数并列最多,你能找到他们所有的\ncustomer_number 吗?\n解析\n这道题需要查询最多订单的客户,表中的订单号order_number是主键,也就是唯一的,而客户号customer_number可以重复,因此相同的客户号的数量即为订单数量,很容易想到使用分组聚合,使用count进行查询每个客户的订单数量,然后再进行倒序排序取第一个即可查询到最多数量的客户号。\nselect customer_numberfrom ordersgroup by customer_numberorder by count(customer_number) desclimit 1;\n题目中进阶要求,如果多位顾客订单数并列最多,把所有的查询出来。首先可以明确,如果多个顾客订单数量都是最多的,上述代码中查询到的是最多数量,因此多个顾客订单数量最多也是这个数的大小。因此只需要再包一层查询进行嵌套,比较一下当前顾客的数量是不是等于当前数量即可。\nselect customer_numberfrom ordersgroup by customer_numberhaving count(customer_number) = ( select count(customer_number) cnt from orders group by customer_number order by cnt desc limit 1);\n略作修改,如果直接对count(customer_number)排序会报错,需要在select中进行查询取别名,然后再对别名进行排序。\n610.判断三角形\n表: Triangle\n+-------------+------+| Column Name | Type |+-------------+------+| x | int || y | int || z | int |+-------------+------+在 SQL 中,(x, y, z)是该表的主键列。该表的每一行包含三个线段的长度。\n对每三个线段报告它们是否可以形成一个三角形。\n以 任意顺序 返回结果表。\n查询结果格式如下所示。\n示例 1:\n输入: Triangle 表:+----+----+----+| x | y | z |+----+----+----+| 13 | 15 | 30 || 10 | 20 | 15 |+----+----+----+输出: +----+----+----+----------+| x | y | z | triangle |+----+----+----+----------+| 13 | 15 | 30 | No || 10 | 20 | 15 | Yes |+----+----+----+----------+\n解析(case when条件判断)\n题目不难,很容易理解。问题在于如何判断并且输出YesorNo。\n这里使用case when进行判断。\nselect x, y, z, case when x + y > z and x + z > y and y + z > x then 'Yes' else 'No' end 'triangle'from triangle;\n619.只出现一次的最大数字\nMyNumbers 表:\n+-------------+------+| Column Name | Type |+-------------+------+| num | int |+-------------+------+该表可能包含重复项(换句话说,在SQL中,该表没有主键)。这张表的每一行都含有一个整数。\n单一数字 是在 MyNumbers\n表中只出现一次的数字。\n找出最大的 单一数字 。如果不存在\n单一数字 ,则返回 null 。\n查询结果如下例所示。\n示例 1:\n输入:MyNumbers 表:+-----+| num |+-----+| 8 || 8 || 3 || 3 || 1 || 4 || 5 || 6 |+-----+输出:+-----+| num |+-----+| 6 |+-----+解释:单一数字有 1、4、5 和 6 。6 是最大的单一数字,返回 6 。\n示例 2:\n输入:MyNumbers table:+-----+| num |+-----+| 8 || 8 || 7 || 7 || 3 || 3 || 3 |+-----+输出:+------+| num |+------+| null |+------+解释:输入的表中不存在单一数字,所以返回 null 。\n解析(max函数)\n题目容易理解,可以分为两部分解决,首先是查询到只出现了一次的数字,然后从中取最大值。\n其中的比较难处理的是如果没有出现一次的数字,需要返回null。\n这里使用嵌套查询和max函数。其中max函数在查询表为空时会返回null值。\nselect max(num) numfrom ( select num from mynumbers group by num having count(num) = 1) t\n1084.销售分析III\n表: Product\n+--------------+---------+| Column Name | Type |+--------------+---------+| product_id | int || product_name | varchar || unit_price | int |+--------------+---------+product_id 是该表的主键(具有唯一值的列)。该表的每一行显示每个产品的名称和价格。\n表:Sales\n+-------------+---------+| Column Name | Type |+-------------+---------+| seller_id | int || product_id | int || buyer_id | int || sale_date | date || quantity | int || price | int |+------ ------+---------+这个表可能有重复的行。product_id 是 Product 表的外键(reference 列)。该表的每一行包含关于一个销售的一些信息。\n编写解决方案,报告2019年春季才售出的产品。即仅在2019-01-01至2019-03-31(含)之间出售的商品。\n以 任意顺序 返回结果表。\n结果格式如下所示。\n示例 1:\n输入:Product table:+------------+--------------+------------+| product_id | product_name | unit_price |+------------+--------------+------------+| 1 | S8 | 1000 || 2 | G4 | 800 || 3 | iPhone | 1400 |+------------+--------------+------------+Sales table:+-----------+------------+----------+------------+----------+-------+| seller_id | product_id | buyer_id | sale_date | quantity | price |+-----------+------------+----------+------------+----------+-------+| 1 | 1 | 1 | 2019-01-21 | 2 | 2000 || 1 | 2 | 2 | 2019-02-17 | 1 | 800 || 2 | 2 | 3 | 2019-06-02 | 1 | 800 || 3 | 3 | 4 | 2019-05-13 | 2 | 2800 |+-----------+------------+----------+------------+----------+-------+输出:+-------------+--------------+| product_id | product_name |+-------------+--------------+| 1 | S8 |+-------------+--------------+解释:id 为 1 的产品仅在 2019 年春季销售。id 为 2 的产品在 2019 年春季销售,但也在 2019 年春季之后销售。id 为 3 的产品在 2019 年春季之后销售。我们只返回 id 为 1 的产品,因为它是 2019 年春季才销售的产品。\n解析(count条件计数)\n这道题是要寻找仅在第一季度销售的产品,转换思路是:在第一季度销售的产品数量和销售的总产品数量是相等的。\n因此可以利用count函数的条件计数来做。\nselect s.product_id, p.product_namefrom Sales sinner join Product pon s.product_id = p.product_idgroup by s.product_idhaving count(s.sale_date between '2019-01-01' and '2019-03-31' or null) = count(*);\n其中count(s.sale_date between '2019-01-01' and '2019-03-31' or null)中的or null是固定写法,不写的话等价于count(*)。也可以写成count(if(s.sale_date between '2019-01-01' and '2019-03-31', 1, null))\nMySQL常用四舍五入函数\n\n\n\n\n\n\n\n函数\n说明\n\n\n\n\nfloor(x)\n返回不大于x的最大整数(下取整)\n\n\nceil(x), ceiling(x)\n返回不小于x的最小整数(上取整)\n\n\ntruncate(x, d)\n返回数值x保留到小数点后d位的值,直接截断,不四舍五入\n\n\nround(x, d)\n保留小数点后d位的值,截断时四舍五入\n\n\nformat(x, d)\n将数字x格式化,保留到小数点后d位,截断时四舍五入\n\n\n\nMySQL常用字符串函数\n\n\n\n\n\n\n\n函数\n说明\n\n\n\n\nleft(str, len)\n字符串从左向右截取len长度的字符串\n\n\nright(str, len)\n字符串从右向左截取len长度的字符串\n\n\nsubstring(str, pos [, len])\n字符串从pos位置开始截取长度为len的字符串,如果len省略,则截取到最后\n\n\nupper(str)\n大写字符串\n\n\nlower(str)\n小写字符串\n\n\nconcat(str1, str2 [, ...])\n连接字符串\n\n\nlength(str)\n返回字符串的存储长度\n\n\nchar_length(str)\n返回字符串的字符长度\n\n\n\nMySQL时间日期函数\n\n\n\n\n\n\n\n函数\n说明\n\n\n\n\ndatediff(endtime, starttime)\n比较两个日期相差多少天,返回endtime - starttime相差的数值\n\n\ntimediff(endtime, starttime)\n比较两个日期相差多长时间,返回endtime -\nstarttime相差的时间,以hh:MM:ss形式返回\n\n\ntimestampdiff(unit, starttime, endtime)\n按照unit指定的单位返回endtime - starttime,如second day等\n\n\nweekday(time)\n返回星期的索引,0表示星期一,1表示星期二,以此类推\n\n\ndate_format(time, format)\ndate_format(now(), '%Y-%m-%d\n%H:%i:%s')格式化成年月日时分秒的形式,注意分钟是%i\n\n\nstr_to_date(time, format)\n将字符串转成日期\n\n\n\nmysql\nbetween and 边界问题-CSDN博客\nSQL语句between\nand边界问题 - 楼兰胡杨 - 博客园 (cnblogs.com)\nsql执行顺序\nmysql\n中的select,from,where,group by等 关键字 执行顺序与别名问题_mysql join\nwhere group 顺序-CSDN博客\nSQL高级:窗口函数+聚合函数 -\n知乎 (zhihu.com)\n1179.重新格式化部门表\n\n\n\n表 Department:\n+---------------+---------+| Column Name | Type |+---------------+---------+| id | int || revenue | int || month | varchar |+---------------+---------+在 SQL 中,(id, month) 是表的联合主键。这个表格有关于每个部门每月收入的信息。月份(month)可以取下列值 [\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"]。\n重新格式化表格,使得 每个月 都有一个部门 id\n列和一个收入列。\n以 任意顺序 返回结果表。\n结果格式如以下示例所示。\n示例 1:\n输入:Department table:+------+---------+-------+| id | revenue | month |+------+---------+-------+| 1 | 8000 | Jan || 2 | 9000 | Jan || 3 | 10000 | Feb || 1 | 7000 | Feb || 1 | 6000 | Mar |+------+---------+-------+输出:+------+-------------+-------------+-------------+-----+-------------+| id | Jan_Revenue | Feb_Revenue | Mar_Revenue | ... | Dec_Revenue |+------+-------------+-------------+-------------+-----+-------------+| 1 | 8000 | 7000 | 6000 | ... | null || 2 | 9000 | null | null | ... | null || 3 | null | 10000 | null | ... | null |+------+-------------+-------------+-------------+-----+-------------+解释:四月到十二月的收入为空。 请注意,结果表共有 13 列(1 列用于部门 ID,其余 12 列用于各个月份)。\n解析(group by原理,行转列)\n一道行转列问题。参考力扣题解。\n首先需要了解group by的原理,可以参考链接。\nselect id , sum(case when month = 'Jan' then revenue end) Jan_Revenue , sum(case when month = 'Feb' then revenue end) Feb_Revenue , sum(case when month = 'Mar' then revenue end) Mar_Revenue , sum(case when month = 'Apr' then revenue end) Apr_Revenue , sum(case when month = 'May' then revenue end) May_Revenue , sum(case when month = 'Jun' then revenue end) Jun_Revenue , sum(case when month = 'Jul' then revenue end) Jul_Revenue , sum(case when month = 'Aug' then revenue end) Aug_Revenue , sum(case when month = 'Sep' then revenue end) Sep_Revenue , sum(case when month = 'Oct' then revenue end) Oct_Revenue , sum(case when month = 'Nov' then revenue end) Nov_Revenue , sum(case when month = 'Dec' then revenue end) Dec_Revenuefrom Departmentgroup by id;\n这里主要解释一下代码中为什么要使用sum聚合函数。\n分组后的虚拟表可以看成如下形式:\n+------+---------+-------+| id | revenue | month |+------+---------+-------+| | 8000 | Jan || 1 | 7000 | Feb || | 6000 | Mar |+------+---------+-------+| 2 | 9000 | Jan |+------+---------+-------+| 3 | 10000 | Feb |+------+---------+-------+\n因为case when条件只读取单元格中第一个数据,而分组后的单元格month列的单元格可能会有多个数据,因此需要遍历单元格所有数据,所以需要使用聚合函数进行遍历。每一个查询属性在单元格中只会对应一个数据或者没有对应数据,也就是结果为revenue或者null,因此结果没有偏差。\n1795.每个产品在不同商店的价格\n表:Products\n+-------------+---------+| Column Name | Type |+-------------+---------+| product_id | int || store1 | int || store2 | int || store3 | int |+-------------+---------+在 SQL 中,这张表的主键是 product_id(产品Id)。每行存储了这一产品在不同商店 store1, store2, store3 的价格。如果这一产品在商店里没有出售,则值将为 null。\n请你重构 Products\n表,查询每个产品在不同商店的价格,使得输出的格式变为(product_id, store, price)\n。如果这一产品在商店里没有出售,则不输出这一行。\n输出结果表中的 顺序不作要求 。\n查询输出格式请参考下面示例。\n示例 1:\n输入:Products table:+------------+--------+--------+--------+| product_id | store1 | store2 | store3 |+------------+--------+--------+--------+| 0 | 95 | 100 | 105 || 1 | 70 | null | 80 |+------------+--------+--------+--------+输出:+------------+--------+-------+| product_id | store | price |+------------+--------+-------+| 0 | store1 | 95 || 0 | store2 | 100 || 0 | store3 | 105 || 1 | store1 | 70 || 1 | store3 | 80 |+------------+--------+-------+解释:产品 0 在 store1、store2、store3 的价格分别为 95、100、105。产品 1 在 store1、store3 的价格分别为 70、80。在 store2 无法买到。\n解析(列转行)\n这是一道列转行问题,解题思路如下:\n\n一列一列处理:把原来的列名作为新列(store)的value,原来的列的value作为另一个新列(price)的value。\n将所有结果union all合并\n值为null的需要跳过\n\nselect product_id, 'store1' store, store1 pricefrom Productswhere store1 is not nullunion allselect product_id, 'store2' store, store2 pricefrom Productswhere store2 is not nullunion allselect product_id, 'store3' store, store3 pricefrom Productswhere store3 is not null;\n这里新列'store1' store等需要加引号,因为这是要把常量值为'store1'的作为store列,如果不加引号,则是认为store1这列的值作为store列。\n1251.平均售价\n表:Prices\n+---------------+---------+| Column Name | Type |+---------------+---------+| product_id | int || start_date | date || end_date | date || price | int |+---------------+---------+(product_id,start_date,end_date) 是 prices 表的主键(具有唯一值的列的组合)。prices 表的每一行表示的是某个产品在一段时期内的价格。每个产品的对应时间段是不会重叠的,这也意味着同一个产品的价格时段不会出现交叉。\n表:UnitsSold\n+---------------+---------+| Column Name | Type |+---------------+---------+| product_id | int || purchase_date | date || units | int |+---------------+---------+该表可能包含重复数据。该表的每一行表示的是每种产品的出售日期,单位和产品 id。\n编写解决方案以查找每种产品的平均售价。average_price 应该\n四舍五入到小数点后两位。\n返回结果表 无顺序要求 。\n结果格式如下例所示。\n示例 1:\n输入:Prices table:+------------+------------+------------+--------+| product_id | start_date | end_date | price |+------------+------------+------------+--------+| 1 | 2019-02-17 | 2019-02-28 | 5 || 1 | 2019-03-01 | 2019-03-22 | 20 || 2 | 2019-02-01 | 2019-02-20 | 15 || 2 | 2019-02-21 | 2019-03-31 | 30 |+------------+------------+------------+--------+UnitsSold table:+------------+---------------+-------+| product_id | purchase_date | units |+------------+---------------+-------+| 1 | 2019-02-25 | 100 || 1 | 2019-03-01 | 15 || 2 | 2019-02-10 | 200 || 2 | 2019-03-22 | 30 |+------------+---------------+-------+输出:+------------+---------------+| product_id | average_price |+------------+---------------+| 1 | 6.96 || 2 | 16.96 |+------------+---------------+解释:平均售价 = 产品总价 / 销售的产品数量。产品 1 的平均售价 = ((100 * 5)+(15 * 20) )/ 115 = 6.96产品 2 的平均售价 = ((200 * 15)+(30 * 30) )/ 230 = 16.96\n解析\n这道题难点在于使用聚合函数将两个表的units和price进行相加再求平均值无法使用聚合函数对两个表操作,因此需要先利用product_id进行左外连接合并成一个表然后进行连接操作。使用左外连接的原因是产品可能没有卖出去,但是定价是存在的,所以保证每个产品都被查询到,需要使用左外连接。连接后需要满足条件,售卖的日期需要是在定价开始日期到结束日期之间的,左外连接是以左表为基础的全连接,会连接出来不满足条件的数据行,因此需要根据这个条件进行过滤,同时为了保证没有售卖的数据可以保留下来,需要再添加一个null的特殊处理。\n一个测试用例,进行左外连接没有过滤的数据如下所示:\n| product_id | start_date | end_date | price | product_id | purchase_date | units || ---------- | ---------- | ---------- | ----- | ---------- | ------------- | ----- || 1 | 2019-02-17 | 2019-02-28 | 5 | 1 | 2019-03-01 | 15 || 1 | 2019-02-17 | 2019-02-28 | 5 | 1 | 2019-02-25 | 100 || 1 | 2019-03-01 | 2019-03-22 | 20 | 1 | 2019-03-01 | 15 || 1 | 2019-03-01 | 2019-03-22 | 20 | 1 | 2019-02-25 | 100 || 2 | 2019-02-01 | 2019-02-20 | 15 | 2 | 2019-03-22 | 30 || 2 | 2019-02-01 | 2019-02-20 | 15 | 2 | 2019-02-10 | 200 || 2 | 2019-02-21 | 2019-03-31 | 30 | 2 | 2019-03-22 | 30 || 2 | 2019-02-21 | 2019-03-31 | 30 | 2 | 2019-02-10 | 200 || 3 | 2019-02-21 | 2019-03-31 | 30 | null | null | null |\n最后使用聚合函数进行求平均价格,注意的是如果该产品没有卖出去,在连接后会出现null的情况,可以使用ifnull函数进行特殊判断。\nselect p.product_id , round(ifnull(sum(units * price) / sum(units), 0), 2) average_pricefrom Prices pleft join UnitsSold uon u.product_id = p.product_idwhere u.purchase_date between p.start_date and p.end_date or u.purchase_date is nullgroup by p.product_id;\n1280.学生们参加各科测试的次数\n学生表: Students\n+---------------+---------+| Column Name | Type |+---------------+---------+| student_id | int || student_name | varchar |+---------------+---------+在 SQL 中,主键为 student_id(学生ID)。该表内的每一行都记录有学校一名学生的信息。\n科目表: Subjects\n+--------------+---------+| Column Name | Type |+--------------+---------+| subject_name | varchar |+--------------+---------+在 SQL 中,主键为 subject_name(科目名称)。每一行记录学校的一门科目名称。\n考试表: Examinations\n+--------------+---------+| Column Name | Type |+--------------+---------+| student_id | int || subject_name | varchar |+--------------+---------+这个表可能包含重复数据(换句话说,在 SQL 中,这个表没有主键)。学生表里的一个学生修读科目表里的每一门科目。这张考试表的每一行记录就表示学生表里的某个学生参加了一次科目表里某门科目的测试。\n查询出每个学生参加每一门科目测试的次数,结果按\nstudent_id 和 subject_name 排序。\n查询结构格式如下所示。\n示例 1:\n输入:Students table:+------------+--------------+| student_id | student_name |+------------+--------------+| 1 | Alice || 2 | Bob || 13 | John || 6 | Alex |+------------+--------------+Subjects table:+--------------+| subject_name |+--------------+| Math || Physics || Programming |+--------------+Examinations table:+------------+--------------+| student_id | subject_name |+------------+--------------+| 1 | Math || 1 | Physics || 1 | Programming || 2 | Programming || 1 | Physics || 1 | Math || 13 | Math || 13 | Programming || 13 | Physics || 2 | Math || 1 | Math |+------------+--------------+输出:+------------+--------------+--------------+----------------+| student_id | student_name | subject_name | attended_exams |+------------+--------------+--------------+----------------+| 1 | Alice | Math | 3 || 1 | Alice | Physics | 2 || 1 | Alice | Programming | 1 || 2 | Bob | Math | 1 || 2 | Bob | Physics | 0 || 2 | Bob | Programming | 1 || 6 | Alex | Math | 0 || 6 | Alex | Physics | 0 || 6 | Alex | Programming | 0 || 13 | John | Math | 1 || 13 | John | Physics | 1 || 13 | John | Programming | 1 |+------------+--------------+--------------+----------------+解释:结果表需包含所有学生和所有科目(即便测试次数为0):Alice 参加了 3 次数学测试, 2 次物理测试,以及 1 次编程测试;Bob 参加了 1 次数学测试, 1 次编程测试,没有参加物理测试;Alex 啥测试都没参加;John 参加了数学、物理、编程测试各 1 次。\n解析\n第一次做这道题,因为样例输出学生没有参加的科目的考试输出次数为0,因此首先想到了使用左外连接,让Students是左表,然后连接另外两个表,但是这只能保证找到所有学生,并不能找到所有学生和所有科目。因此需要转变思路。\n要保证有所有学生的所有科目,而Students和Subjects表是完整的学生和科目数据,因此可以使用交叉连接获得所有学生的考所有科目的信息,然后再从Examinations表中获取每个学生考每科的次数,进行连接即可。\nselect stu.student_id , stu.student_name , sub.subject_name , ifnull(exams.attended_exams, 0) attended_examsfrom Students stucross join Subjects subleft join ( select student_id, subject_name, count(*) attended_exams from Examinations group by student_id, subject_name) examson stu.student_id = exams.student_idand sub.subject_name = exams.subject_nameorder by stu.student_id, sub.subject_name;\n参考了其他人的题解后,对代码进行了修改。\nselect stu.student_id , stu.student_name , sub.subject_name , count(exams.subject_name) attended_examsfrom Students stucross join Subjects subleft join Examinations examson stu.student_id = exams.student_idand sub.subject_name = exams.subject_namegroup by stu.student_id, sub.subject_nameorder by stu.student_id, sub.subject_name;\n1484.按日期分组销售产品\n表 Activities:\n+-------------+---------+| 列名 | 类型 |+-------------+---------+| sell_date | date || product | varchar |+-------------+---------+该表没有主键(具有唯一值的列)。它可能包含重复项。此表的每一行都包含产品名称和在市场上销售的日期。\n编写解决方案找出每个日期、销售的不同产品的数量及其名称。\n每个日期的销售产品名称应按词典序排列。 返回按 sell_date\n排序的结果表。 结果表结果格式如下例所示。\n示例 1:\n输入:Activities 表:+------------+-------------+| sell_date | product |+------------+-------------+| 2020-05-30 | Headphone || 2020-06-01 | Pencil || 2020-06-02 | Mask || 2020-05-30 | Basketball || 2020-06-01 | Bible || 2020-06-02 | Mask || 2020-05-30 | T-Shirt |+------------+-------------+输出:+------------+----------+------------------------------+| sell_date | num_sold | products |+------------+----------+------------------------------+| 2020-05-30 | 3 | Basketball,Headphone,T-shirt || 2020-06-01 | 2 | Bible,Pencil || 2020-06-02 | 1 | Mask |+------------+----------+------------------------------+解释:对于2020-05-30,出售的物品是 (Headphone, Basketball, T-shirt),按词典序排列,并用逗号 ',' 分隔。对于2020-06-01,出售的物品是 (Pencil, Bible),按词典序排列,并用逗号分隔。对于2020-06-02,出售的物品是 (Mask),只需返回该物品名。\n解析(group_concat函数)\n这道题难点在于group_concat函数的使用。参考了博客\n这个函数的语法范式:group_concat([distinct] 需要连接的字段名 [order by 排序字段 desc/asc] [separator '分隔符'])\nselect sell_date , count(distinct product) num_sold , group_concat( distinct product order by product asc separator ',' ) productsfrom Activitiesgroup by sell_date;\n1517.查找拥有有效邮箱的用户\n表: Users\n+---------------+---------+| Column Name | Type |+---------------+---------+| user_id | int || name | varchar || mail | varchar |+---------------+---------+user_id 是该表的主键(具有唯一值的列)。该表包含了网站已注册用户的信息。有一些电子邮件是无效的。\n编写一个解决方案,以查找具有有效电子邮件的用户。\n一个有效的电子邮件具有前缀名称和域,其中:\n\n前缀\n名称是一个字符串,可以包含字母(大写或小写),数字,下划线\n'_' ,点 '.' 和/或破折号 '-'\n。前缀名称 必须 以字母开头。\n域 为 '@leetcode.com' 。\n\n以任何顺序返回结果表。\n结果的格式如以下示例所示:\n示例 1:\n输入:Users 表:+---------+-----------+-------------------------+| user_id | name | mail |+---------+-----------+-------------------------+| 1 | Winston | [email protected] || 2 | Jonathan | jonathanisgreat || 3 | Annabelle | [email protected] || 4 | Sally | [email protected] || 5 | Marwan | quarz#[email protected] || 6 | David | [email protected] || 7 | Shapiro | [email protected] |+---------+-----------+-------------------------+输出:+---------+-----------+-------------------------+| user_id | name | mail |+---------+-----------+-------------------------+| 1 | Winston | [email protected] || 3 | Annabelle | [email protected] || 4 | Sally | [email protected] |+---------+-----------+-------------------------+解释:用户 2 的电子邮件没有域。 用户 5 的电子邮件带有不允许的 '#' 符号。用户 6 的电子邮件没有 leetcode 域。 用户 7 的电子邮件以点开头。\n解析(正则表达式的应用)\n^ 表示以后面的字符为开头 []\n表示括号内任意字符\n- 表示连续\n* 表示重复前面任意字符任意次数 \\\n用来转义后面的特殊字符,以表示字符原本的样子,而不是将其作为特殊字符使用\n$ 表示以前面的字符为结尾\n因此,前缀必须以字母开头可以表示^[a-zA-Z],前缀名包含若干数字、下划线、句点、横杠[a-zA-Z0-9_\\.\\-]*,以@leetcode.com结尾则是\\@leetcode\\.com$。因为. - @属于特殊字符,要使用本身的时候需要加\\转义。\nselect *from Userswhere mail regexp '^[a-zA-Z][a-zA-Z0-9_\\\\.\\\\-]*\\\\@leetcode\\\\.com$';\n正则表达式中使用\\.进行转义,而在MySQL中需要对\\进行转义,因此需要再加一个\\。\n1527.患某种疾病的患者\n患者信息表: Patients\n+--------------+---------+| Column Name | Type |+--------------+---------+| patient_id | int || patient_name | varchar || conditions | varchar |+--------------+---------+在 SQL 中,patient_id (患者 ID)是该表的主键。'conditions' (疾病)包含 0 个或以上的疾病代码,以空格分隔。这个表包含医院中患者的信息。\n查询患有 I 类糖尿病的患者 ID\n(patient_id)、患者姓名(patient_name)以及其患有的所有疾病代码(conditions)。I\n类糖尿病的代码总是包含前缀 DIAB1 。\n按 任意顺序 返回结果表。\n查询结果格式如下示例所示。\n示例 1:\n输入:Patients表:+------------+--------------+--------------+| patient_id | patient_name | conditions |+------------+--------------+--------------+| 1 | Daniel | YFEV COUGH || 2 | Alice | || 3 | Bob | DIAB100 MYOP || 4 | George | ACNE DIAB100 || 5 | Alain | DIAB201 |+------------+--------------+--------------+输出:+------------+--------------+--------------+| patient_id | patient_name | conditions |+------------+--------------+--------------+| 3 | Bob | DIAB100 MYOP || 4 | George | ACNE DIAB100 | +------------+--------------+--------------+解释:Bob 和 George 都患有代码以 DIAB1 开头的疾病。\n解析(like)\n题目很简单,难点在于如何匹配conditions中的字符串。在每一个疾病代码中,要有以DIAB1开头的才可以,而不是包含这个子字符串。因此可以用两种情况考虑:\n\n排在第一个疾病代码中,所以使用like匹配是like 'DIAB1%'\n排在非第一个疾病代码中,所以使用like匹配是like '% DIAB1%',因为每一个疾病代码使用空格分开,所以保证以该字符串开头,需要在前边加上空格。\n\nselect *from Patientswhere conditions like 'DIAB1%' or conditions like '% DIAB1%';\n方法二:\n可以使用正则表达式进行匹配,正则表达式:^DIAB1|\\sDIAB1\nselect *from Patientswhere conditions regexp '^DIAB1|\\\\sDIAB1';\n1661.每台机器的进程平均运行时间\n表: Activity\n+----------------+---------+| Column Name | Type |+----------------+---------+| machine_id | int || process_id | int || activity_type | enum || timestamp | float |+----------------+---------+该表展示了一家工厂网站的用户活动。(machine_id, process_id, activity_type) 是当前表的主键(具有唯一值的列的组合)。machine_id 是一台机器的ID号。process_id 是运行在各机器上的进程ID号。activity_type 是枚举类型 ('start', 'end')。timestamp 是浮点类型,代表当前时间(以秒为单位)。'start' 代表该进程在这台机器上的开始运行时间戳 , 'end' 代表该进程在这台机器上的终止运行时间戳。同一台机器,同一个进程都有一对开始时间戳和结束时间戳,而且开始时间戳永远在结束时间戳前面。\n现在有一个工厂网站由几台机器运行,每台机器上运行着\n相同数量的进程\n。编写解决方案,计算每台机器各自完成一个进程任务的平均耗时。\n完成一个进程任务的时间指进程的'end' 时间戳 减去\n'start' 时间戳。平均耗时通过计算每台机器上所有进程任务的总耗费时间除以机器上的总进程数量获得。\n结果表必须包含machine_id(机器ID) 和对应的\naverage time(平均耗时) 别名\nprocessing_time,且四舍五入保留3位小数。\n以 任意顺序 返回表。\n具体参考例子如下。\n示例 1:\n输入:Activity table:+------------+------------+---------------+-----------+| machine_id | process_id | activity_type | timestamp |+------------+------------+---------------+-----------+| 0 | 0 | start | 0.712 || 0 | 0 | end | 1.520 || 0 | 1 | start | 3.140 || 0 | 1 | end | 4.120 || 1 | 0 | start | 0.550 || 1 | 0 | end | 1.550 || 1 | 1 | start | 0.430 || 1 | 1 | end | 1.420 || 2 | 0 | start | 4.100 || 2 | 0 | end | 4.512 || 2 | 1 | start | 2.500 || 2 | 1 | end | 5.000 |+------------+------------+---------------+-----------+输出:+------------+-----------------+| machine_id | processing_time |+------------+-----------------+| 0 | 0.894 || 1 | 0.995 || 2 | 1.456 |+------------+-----------------+解释:一共有3台机器,每台机器运行着两个进程.机器 0 的平均耗时: ((1.520 - 0.712) + (4.120 - 3.140)) / 2 = 0.894机器 1 的平均耗时: ((1.550 - 0.550) + (1.420 - 0.430)) / 2 = 0.995机器 2 的平均耗时: ((4.512 - 4.100) + (5.000 - 2.500)) / 2 = 1.456\n解析\n题目很容易看懂,难点在于如何找到每个进程的运行时间,也就是某个machine_id下的某个process_id的结束时间戳减开始时间戳。原来的表这两个值是在同一列,无法直接减,需要通过操作将这两个值放在同一行进行操作。\n可以使用自连接操作,在连接时对连接条件进行判断,手动指定一个表留下开始时间戳,另一个表留下结束时间戳,然后根据machine_id分组,最后计算平均值即可。\nselect a1.machine_id, round(avg(a2.timestamp - a1.timestamp), 3) processing_timefrom Activity a1inner join Activity a2on a1.machine_id = a2.machine_idand a1.process_id = a2.process_idand a1.activity_type = 'start'and a2.activity_type = 'end'group by a1.machine_id;\n使用avg函数可以直接计算平均值。他会遍历一个分组的所有的行,每行执行的操作是a2.timestamp - a1.timestamp,最后计算平均值。如有不懂可以再看一下1179题。\n1789.员工的直属部门\n表:Employee\n+---------------+---------+| Column Name | Type |+---------------+---------+| employee_id | int || department_id | int || primary_flag | varchar |+---------------+---------+这张表的主键为 employee_id, department_id (具有唯一值的列的组合)employee_id 是员工的IDdepartment_id 是部门的ID,表示员工与该部门有关系primary_flag 是一个枚举类型,值分别为('Y', 'N'). 如果值为'Y',表示该部门是员工的直属部门。 如果值是'N',则否\n一个员工可以属于多个部门。当一个员工加入超过一个部门的时候,他需要决定哪个部门是他的直属部门。请注意,当员工只加入一个部门的时候,那这个部门将默认为他的直属部门,虽然表记录的值为'N'.\n请编写解决方案,查出员工所属的直属部门。\n返回结果 没有顺序要求 。\n返回结果格式如下例子所示:\n示例 1:\n输入:Employee table:+-------------+---------------+--------------+| employee_id | department_id | primary_flag |+-------------+---------------+--------------+| 1 | 1 | N || 2 | 1 | Y || 2 | 2 | N || 3 | 3 | N || 4 | 2 | N || 4 | 3 | Y || 4 | 4 | N |+-------------+---------------+--------------+输出:+-------------+---------------+| employee_id | department_id |+-------------+---------------+| 1 | 1 || 2 | 1 || 3 | 3 || 4 | 3 |+-------------+---------------+解释:- 员工 1 的直属部门是 1- 员工 2 的直属部门是 1- 员工 3 的直属部门是 3- 员工 4 的直属部门是 3\n解析(union)\n这道题分析可以了解到,一个员工的直属部门分为两种情况:\n\n只有一个部门的,无论primary_flag是Y还是N,直属部门就是department_id。\n有多个部门的,当primary_flag是Y的为直属部门。\n\n因此可以使用union合并查询结果,第一次查询部门只有一个的员工,那么他的直属部门是确定的;第二次查询primary_key是Y的,那么他的直属部门也是确定的了。如果部门只有一个的员工中的primary_flag是Y,那么第二次查询也会查到这个数据,但是通过union操作会去除重复数据,因此不会影响最终结果。\nselect employee_id, department_idfrom Employeegroup by employee_idhaving count(*) = 1unionselect employee_id, department_idfrom Employeewhere primary_flag = 'Y';\n看题解了解到也可以使用窗口函数解答,但是目前没有学过窗口函数,先埋个坑🕳。\n180.连续出现的数字\n\n\n\n表:Logs\n+-------------+---------+| Column Name | Type |+-------------+---------+| id | int || num | varchar |+-------------+---------+在 SQL 中,id 是该表的主键。id 是一个自增列。\n找出所有至少连续出现三次的数字。\n返回的结果表中的数据可以按 任意顺序 排列。\n结果格式如下面的例子所示:\n示例 1:\n输入:Logs 表:+----+-----+| id | num |+----+-----+| 1 | 1 || 2 | 1 || 3 | 1 || 4 | 2 || 5 | 1 || 6 | 2 || 7 | 2 |+----+-----+输出:Result 表:+-----------------+| ConsecutiveNums |+-----------------+| 1 |+-----------------+解释:1 是唯一连续出现至少三次的数字。\n解析(连续n问题,窗口函数)\n窗口函数的内容可以参考这篇博客。\n连续n问题解题思路首先是利用窗口函数构造一个新的列,给每个num编号。\nselect \t*\t, row_number() over (partition by num order by id) id1from Logs\n上述查询会构建一个新的列,按照num分组,id升序的顺序重新编号。\n| id | num | id1 || -- | --- | --- || 1 | 1 | 1 || 2 | 1 | 2 || 3 | 1 | 3 || 5 | 1 | 4 || 4 | 2 | 1 || 6 | 2 | 2 || 7 | 2 | 3 |\n观察id - id1的值,可以发现,连续相同的数的这个差值是相同的。但是仍然会有问题,题目中id为自增主键,自增主键不一定完全连续,如果中间断开,则上述规律可能会不成立。因此可以再利用窗口函数row_number()构造一个新的列,使其完全连续。\nselect * , row_number() over (partition by num order by id) id1 , row_number() over (order by id) id2from Logs\n第二次窗口函数构建的列没有进行划分分组,直接按照id升序进行编号。即使id从中间某个数断开,id2也并不会断开。因此id2 - id1的差值满足以上规律。\n| id | num | id1 | id2 || -- | --- | --- | --- || 1 | 1 | 1 | 1 || 2 | 1 | 2 | 2 || 3 | 1 | 3 | 3 || 4 | 2 | 1 | 4 || 5 | 1 | 4 | 5 || 6 | 2 | 2 | 6 || 7 | 2 | 3 | 7 |\n实际测试中,如果只按照id2 - id1进行分组,仍然会出问题。\n如以下数据:\n+----+-----+-----+-----+| id | num | id1 | id2 |+----+-----+-----+-----+| 1 | 1 | 1 | 1 || 2 | 2 | 1 | 2 || 3 | 1 | 2 | 3 || 4 | 1 | 3 | 4 |+----+-----+-----+-----+\n因为窗口函数按照num分组后,即使id = 1 3 4的数据从中间断开,但是编号并没有断开,导致id = 2 3 4的数并不相同连续,但是id2 - id1却是相同的。因此最终分组时要按照id2 - id1和num分组,即使差值相同,但是num不同,仍然可以区分出来。\n最终的sql为\nselect distinct num ConsecutiveNumsfrom ( select * , row_number() over (partition by num order by id) id1 , row_number() over (order by id) id2 from Logs) tmp_logsgroup by id2 - id1, numhaving count(*) >= 3;\n最后查询结果需要使用distinct因为后边的连续的数字中可能会有和前边的相同的,结果只要有一个数字即可,因此需要去重。\n626.换座位\n表: Seat\n+-------------+---------+| Column Name | Type |+-------------+---------+| id | int || student | varchar |+-------------+---------+id 是该表的主键(唯一值)列。该表的每一行都表示学生的姓名和 ID。id 是一个连续的增量。\n编写解决方案来交换每两个连续的学生的座位号。如果学生的数量是奇数,则最后一个学生的id不交换。\n按 id 升序 返回结果表。\n查询结果格式如下所示。\n示例 1:\n输入: Seat 表:+----+---------+| id | student |+----+---------+| 1 | Abbot || 2 | Doris || 3 | Emerson || 4 | Green || 5 | Jeames |+----+---------+输出: +----+---------+| id | student |+----+---------+| 1 | Doris || 2 | Abbot || 3 | Green || 4 | Emerson || 5 | Jeames |+----+---------+解释:请注意,如果学生人数为奇数,则不需要更换最后一名学生的座位。\n解析(位运算求奇偶相邻)\n题意很简单,而且id为连续上升的,所以很容易想到将偶数的id减一,将奇数的id加一,然后合并结果。但是这样会导致奇数人数情况下最后一个学生座位发生变化,因此最后套一层窗口函数进行一次排名即可。\nselect row_number() over (order by id) id , studentfrom ( select id + 1 id , student from Seat where mod(id, 2) = 1 union select id - 1 id , student from Seat where mod(id, 2) = 0) torder by id;\n在看了别人提交的代码后,学习到了一种更为巧妙的方法。\nSELECT RANK() OVER (ORDER BY (id - 1) ^ 1) AS id, studentFROM seat\n题目中id连续,所以交换位置可以看作是1和2\n3和4...交换,也就是将两个的id互换。这里运用了位运算,巧妙地实现了这个操作。\n首先让id - 1,所以id是从0, 1, 2开始,然后和1进行异或操作。\n0 = 0b00000000 ^ 0b00000001 = 0b00000001 = 1\n1 = 0b00000001 ^ 0b00000001 = 0b00000000 = 0\n2 = 0b00000010 ^ 0b00000001 = 0b00000011 = 3\n3 = 0b00000011 ^ 0b00000001 = 0b00000010 = 2\n巧妙地将两个相邻的数对换,最后再使用窗口函数进行一次排名即可。\n一个奇数与1做异或后得到的是比它小的相邻偶数,一个偶数与1做异或后得到的是比它大的相邻奇数。\n# Write your MySQL query statement belowSELECT s1.id, COALESCE(s2.student, s1.student) studentFROM Seat s1LEFT JOIN Seat s2ON s1.id - 1 = (s2.id - 1) ^ 1ORDER BY s1.id\n使用上述规律做连接,将每个座位的匹配到交换后的位置,这里让字段先减1再做位运算,因为上述规律是从0开始算,这里是从1开始。\n1070.产品销售分析III\n销售表 Sales:\n+-------------+-------+| Column Name | Type |+-------------+-------+| sale_id | int || product_id | int || year | int || quantity | int || price | int |+-------------+-------+(sale_id, year) 是这张表的主键(具有唯一值的列的组合)。product_id 是产品表的外键(reference 列)。这张表的每一行都表示:编号 product_id 的产品在某一年的销售额。请注意,价格是按每单位计的。\n产品表 Product:\n+--------------+---------+| Column Name | Type |+--------------+---------+| product_id | int || product_name | varchar |+--------------+---------+product_id 是这张表的主键(具有唯一值的列)。这张表的每一行都标识:每个产品的 id 和 产品名称。\n编写解决方案,选出每个售出过的产品 第一年 销售的\n产品 id、年份、数量\n和 价格。\n结果表中的条目可以按 任意顺序 排列。\n结果格式如下例所示:\n示例 1:\n输入:Sales 表:+---------+------------+------+----------+-------+| sale_id | product_id | year | quantity | price |+---------+------------+------+----------+-------+ | 1 | 100 | 2008 | 10 | 5000 || 2 | 100 | 2009 | 12 | 5000 || 7 | 200 | 2011 | 15 | 9000 |+---------+------------+------+----------+-------+Product 表:+------------+--------------+| product_id | product_name |+------------+--------------+| 100 | Nokia || 200 | Apple || 300 | Samsung |+------------+--------------+输出:+------------+------------+----------+-------+| product_id | first_year | quantity | price |+------------+------------+----------+-------+ | 100 | 2008 | 10 | 5000 || 200 | 2011 | 15 | 9000 |+------------+------------+----------+-------+\n解析(row_number()和rank()的异同)\n这道题要找产品第一年的销售情况,可以利用窗口函数,给每个产品按照年份分组,然后进行排序。注意这里要使用rank()而不是row_number()。\nrank函数进行order by排序时,遇到相同值排名不会变化,直到遇到不同值排名会变化,并且会比上一个排名多重复值的长度。而row_number会一直递增。\n这个题要查到的是每种第一年卖出的产品的情况,可能会存在某种产品第一年卖出去多次,这个也需要查出来。因此需要使用rank。\nselect product_id, year first_year, quantity, pricefrom ( select * , rank() over (partition by product_id order by year) rownum from Sales) twhere rownum = 1;\n1321.餐馆营业额变化增长\n表: Customer\n+---------------+---------+| Column Name | Type |+---------------+---------+| customer_id | int || name | varchar || visited_on | date || amount | int |+---------------+---------+在 SQL 中,(customer_id, visited_on) 是该表的主键。该表包含一家餐馆的顾客交易数据。visited_on 表示 (customer_id) 的顾客在 visited_on 那天访问了餐馆。amount 是一个顾客某一天的消费总额。\n你是餐馆的老板,现在你想分析一下可能的营业额变化增长(每天至少有一位顾客)。\n计算以 7 天(某日期 + 该日期前的 6\n天)为一个时间段的顾客消费平均值。average_amount 要\n保留两位小数。\n结果按 visited_on 升序排序。\n返回结果格式的例子如下。\n示例 1:\n输入:Customer 表:+-------------+--------------+--------------+-------------+| customer_id | name | visited_on | amount |+-------------+--------------+--------------+-------------+| 1 | Jhon | 2019-01-01 | 100 || 2 | Daniel | 2019-01-02 | 110 || 3 | Jade | 2019-01-03 | 120 || 4 | Khaled | 2019-01-04 | 130 || 5 | Winston | 2019-01-05 | 110 | | 6 | Elvis | 2019-01-06 | 140 | | 7 | Anna | 2019-01-07 | 150 || 8 | Maria | 2019-01-08 | 80 || 9 | Jaze | 2019-01-09 | 110 | | 1 | Jhon | 2019-01-10 | 130 | | 3 | Jade | 2019-01-10 | 150 | +-------------+--------------+--------------+-------------+输出:+--------------+--------------+----------------+| visited_on | amount | average_amount |+--------------+--------------+----------------+| 2019-01-07 | 860 | 122.86 || 2019-01-08 | 840 | 120 || 2019-01-09 | 840 | 120 || 2019-01-10 | 1000 | 142.86 |+--------------+--------------+----------------+解释:第一个七天消费平均值从 2019-01-01 到 2019-01-07 是restaurant-growth/restaurant-growth/ (100 + 110 + 120 + 130 + 110 + 140 + 150)/7 = 122.86第二个七天消费平均值从 2019-01-02 到 2019-01-08 是 (110 + 120 + 130 + 110 + 140 + 150 + 80)/7 = 120第三个七天消费平均值从 2019-01-03 到 2019-01-09 是 (120 + 130 + 110 + 140 + 150 + 80 + 110)/7 = 120第四个七天消费平均值从 2019-01-04 到 2019-01-10 是 (130 + 110 + 140 + 150 + 80 + 110 + 130 + 150)/7 = 142.86\n解析(窗口函数范围)\n题目要求当前日期及前7天的营业额的平均值。很容易想到使用窗口函数,同时这道题也作为180题的补充\n[操作] over (partition by <列名> order by <列名> rows <窗口滑动的数据范围>)\n其中滑动范围用来限定要操作的数据的范围。\n当前行 - current row之前的行 - preceding之后的行 - following无界限 - unbounded表示从前面的起点 - unbounded preceding表示到后面的终点 - unbounded following\n例:\n取当前行和前五行:ROWS between 5 preceding and current row --共6行取当前行和后五行:ROWS between current row and 5 following --共6行取前五行和后五行:ROWS between 5 preceding and 5 folowing --共11行\n因此最后本题查询如下:\nselect visited_on , sum_amount amount , round(sum_amount / 7, 2) average_amountfrom ( select visited_on , sum(amount) over (order by visited_on rows 6 preceding) sum_amount from ( select visited_on, sum(amount) amount from Customer group by visited_on ) t) ttwhere datediff(visited_on, (select min(visited_on) from Customer)) >= 6;\n最内层查询首先根据日期分组,获取每天的销售总额,因为可能存在同一天有多个客户的情况。\n然后对子查询进行窗口函数计算。计算从当前行和前六行的和,虽然前6行往前不足6行,但是依旧可以计算,有多少行计算多少行。从第7行开始,计算的就是完整的7天的营业额。最后取出满足条件的日期即可。\n本题说明了每天都至少有一个顾客的订单,所以日期肯定是连续的。\n也可以不适用窗口函数,采用自连接方法,参考题解。\n1341.电影评分\n表:Movies\n+---------------+---------+| Column Name | Type |+---------------+---------+| movie_id | int || title | varchar |+---------------+---------+movie_id 是这个表的主键(具有唯一值的列)。title 是电影的名字。\n表:Users\n+---------------+---------+| Column Name | Type |+---------------+---------+| user_id | int || name | varchar |+---------------+---------+user_id 是表的主键(具有唯一值的列)。\n表:MovieRating\n+---------------+---------+| Column Name | Type |+---------------+---------+| movie_id | int || user_id | int || rating | int || created_at | date |+---------------+---------+(movie_id, user_id) 是这个表的主键(具有唯一值的列的组合)。这个表包含用户在其评论中对电影的评分 rating 。created_at 是用户的点评日期。 \n请你编写一个解决方案:\n\n查找评论电影数量最多的用户名。如果出现平局,返回字典序较小的用户名。\n查找在 February 2020 平均评分最高\n的电影名称。如果出现平局,返回字典序较小的电影名称。\n\n字典序\n,即按字母在字典中出现顺序对字符串排序,字典序较小则意味着排序靠前。\n返回结果格式如下例所示。\n示例 1:\n输入:Movies 表:+-------------+--------------+| movie_id | title |+-------------+--------------+| 1 | Avengers || 2 | Frozen 2 || 3 | Joker |+-------------+--------------+Users 表:+-------------+--------------+| user_id | name |+-------------+--------------+| 1 | Daniel || 2 | Monica || 3 | Maria || 4 | James |+-------------+--------------+MovieRating 表:+-------------+--------------+--------------+-------------+| movie_id | user_id | rating | created_at |+-------------+--------------+--------------+-------------+| 1 | 1 | 3 | 2020-01-12 || 1 | 2 | 4 | 2020-02-11 || 1 | 3 | 2 | 2020-02-12 || 1 | 4 | 1 | 2020-01-01 || 2 | 1 | 5 | 2020-02-17 | | 2 | 2 | 2 | 2020-02-01 | | 2 | 3 | 2 | 2020-03-01 || 3 | 1 | 3 | 2020-02-22 | | 3 | 2 | 4 | 2020-02-25 | +-------------+--------------+--------------+-------------+输出:Result 表:+--------------+| results |+--------------+| Daniel || Frozen 2 |+--------------+解释:Daniel 和 Monica 都点评了 3 部电影(\"Avengers\", \"Frozen 2\" 和 \"Joker\") 但是 Daniel 字典序比较小。Frozen 2 和 Joker 在 2 月的评分都是 3.5,但是 Frozen 2 的字典序比较小。\n解析(union子句)\n这道题思路很简单,两次查询,分别查出评分最多的人和评分最高的电影,最后使用union合并。\n但是实际运行时会报错,原因是子句使用了order by\nlimit等,使用这些后不能直接和union操作,因此需要使用括号将两个子句括起来再合并。\n( select u.name results from MovieRating mr inner join Users u on mr.user_id = u.user_id group by mr.user_id order by count(*) desc, u.name asc limit 1)union all( select m.title results from MovieRating mr inner join Movies m on mr.movie_id = m.movie_id where date_format(mr.created_at, '%Y-%m') = '2020-02' group by mr.movie_id order by avg(rating) desc, m.title asc limit 1);\n550. 游戏玩法分析IV\nTable: Activity\n+--------------+---------+| Column Name | Type |+--------------+---------+| player_id | int || device_id | int || event_date | date || games_played | int |+--------------+---------+(player_id,event_date)是此表的主键(具有唯一值的列的组合)。这张表显示了某些游戏的玩家的活动情况。每一行是一个玩家的记录,他在某一天使用某个设备注销之前登录并玩了很多游戏(可能是 0)。\n编写解决方案,报告在首次登录的第二天再次登录的玩家的\n比率,四舍五入到小数点后两位。换句话说,你需要计算从首次登录日期开始至少连续两天登录的玩家的数量,然后除以玩家总数。\n结果格式如下所示:\n示例 1:\n输入:Activity table:+-----------+-----------+------------+--------------+| player_id | device_id | event_date | games_played |+-----------+-----------+------------+--------------+| 1 | 2 | 2016-03-01 | 5 || 1 | 2 | 2016-03-02 | 6 || 2 | 3 | 2017-06-25 | 1 || 3 | 1 | 2016-03-02 | 0 || 3 | 4 | 2018-07-03 | 5 |+-----------+-----------+------------+--------------+输出:+-----------+| fraction |+-----------+| 0.33 |+-----------+解释:只有 ID 为 1 的玩家在第一天登录后才重新登录,所以答案是 1/3 = 0.33\n解析\n先求出每个玩家首次登录日期,然后进行左连接,判断每个玩家是否在第二天登录,如果未登录,则第二个表的字段会为null,依次可以判断最后的比率。\n# Write your MySQL query statement belowSELECT ROUND(AVG(a.player_id IS NOT NULL), 2) fractionFROM ( SELECT player_id, MIN(event_date) event_date FROM Activity GROUP BY player_id) t_loginLEFT JOIN Activity aON DATEDIFF(a.event_date, t_login.event_date) = 1AND t_login.player_id = a.player_id;\n1164.指定日期的产品价格\n产品数据表: Products\n+---------------+---------+| Column Name | Type |+---------------+---------+| product_id | int || new_price | int || change_date | date |+---------------+---------+(product_id, change_date) 是此表的主键(具有唯一值的列组合)。这张表的每一行分别记录了 某产品 在某个日期 更改后 的新价格。\n编写一个解决方案,找出在 2019-08-16\n时全部产品的价格,假设所有产品在修改前的价格都是 10\n。\n以 任意顺序 返回结果表。\n结果格式如下例所示。\n示例 1:\n输入:Products 表:+------------+-----------+-------------+| product_id | new_price | change_date |+------------+-----------+-------------+| 1 | 20 | 2019-08-14 || 2 | 50 | 2019-08-14 || 1 | 30 | 2019-08-15 || 1 | 35 | 2019-08-16 || 2 | 65 | 2019-08-17 || 3 | 20 | 2019-08-18 |+------------+-----------+-------------+输出:+------------+-------+| product_id | price |+------------+-------+| 2 | 50 || 1 | 35 || 3 | 10 |+------------+-------+\n解析\n# Write your MySQL query statement belowSELECT product_id, IF(filter_date IS NULL, 10, new_price) priceFROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY filter_date DESC) rn FROM ( SELECT *, IF(DATEDIFF('2019-08-16', change_date) >= 0, change_date, NULL) filter_date FROM Products ) T1) T2WHERE rn = 1;\n首先排除不符合条件的日期,将其设置为NULL,然后使用窗口函数对其进行倒序排序加索引,这样就可以找到在规定日期之前的最近的日期,如果某个商品所有的日期都在规定日期之后,那么它的filter_date始终为NULL,进行排名后,无论哪个是1,都不会影响最终结果。\n排序时,先按照不是NULL的值进行排序,然后其余为NULL的依次排列。\n585. 2016年的投资\nInsurance 表:\n+-------------+-------+| Column Name | Type |+-------------+-------+| pid | int || tiv_2015 | float || tiv_2016 | float || lat | float || lon | float |+-------------+-------+pid 是这张表的主键(具有唯一值的列)。表中的每一行都包含一条保险信息,其中:pid 是投保人的投保编号。tiv_2015 是该投保人在 2015 年的总投保金额,tiv_2016 是该投保人在 2016 年的总投保金额。lat 是投保人所在城市的纬度。题目数据确保 lat 不为空。lon 是投保人所在城市的经度。题目数据确保 lon 不为空。\n编写解决方案报告 2016 年 (tiv_2016)\n所有满足下述条件的投保人的投保金额之和:\n\n他在 2015 年的投保额 (tiv_2015) 至少跟一个其他投保人在\n2015 年的投保额相同。\n他所在的城市必须与其他投保人都不同(也就是说 (lat, lon)\n不能跟其他任何一个投保人完全相同)。\n\ntiv_2016 四舍五入的 两位小数 。\n查询结果格式如下例所示。\n示例 1:\n输入:Insurance 表:+-----+----------+----------+-----+-----+| pid | tiv_2015 | tiv_2016 | lat | lon |+-----+----------+----------+-----+-----+| 1 | 10 | 5 | 10 | 10 || 2 | 20 | 20 | 20 | 20 || 3 | 10 | 30 | 20 | 20 || 4 | 10 | 40 | 40 | 40 |+-----+----------+----------+-----+-----+输出:+----------+| tiv_2016 |+----------+| 45.00 |+----------+解释:表中的第一条记录和最后一条记录都满足两个条件。tiv_2015 值为 10 与第三条和第四条记录相同,且其位置是唯一的。第二条记录不符合任何一个条件。其 tiv_2015 与其他投保人不同,并且位置与第三条记录相同,这也导致了第三条记录不符合题目要求。因此,结果是第一条记录和最后一条记录的 tiv_2016 之和,即 45 。\n解析\n需要求的是满足两个条件的投保人的投保金额总和。\n使用group by 和 count判断唯一和不唯一这两个条件。分别求出满足两个条件的信息,然后进行过滤。\n# Write your MySQL query statement belowSELECT ROUND(SUM(tiv_2016), 2) tiv_2016FROM InsuranceWHERE CONCAT(lat, lon) IN ( SELECT CONCAT(lat, lon) FROM Insurance GROUP BY lat, lon HAVING COUNT(*) = 1)AND tiv_2015 IN ( SELECT tiv_2015 FROM Insurance GROUP BY tiv_2015 HAVING COUNT(*) > 1)\n❗注意:以下写法是错误的\n# Write your MySQL query statement belowSELECT ROUND(SUM(tiv_2016), 2) tiv_2016FROM ( SELECT SUM(tiv_2016) tiv_2016 FROM ( SELECT * FROM Insurance GROUP BY lat, lon HAVING COUNT(*) = 1 ) T1 GROUP BY tiv_2015 HAVING COUNT(*) > 1) T2\n该写法是先求出地理位置唯一的投保人,然后在地理位置唯一的人里判断tiv_2015这个是不是唯一的,这是错误的。\n\n\n\nid\ntiv_2015\nlat, lon\n\n\n\n\n1\n1\n1,2\n\n\n2\n1\n1,3\n\n\n3\n1\n1,3\n\n\n\n如该数据,其中id=1是满足条件的。但是如果按照上述错误做法来做的话,首先排除掉了id=2,3,然后判断tiv_2015,会发现排除了id=2,3后没有重复,但实际上是有重复的。\n该题本质上两个条件的比对是比对的在全局情况下是否唯一。而不是排除某些后剩下的数据里(不)唯一。\n","categories":["LeetCode"],"tags":["LeetCode","sql"]},{"title":"牛客网SQL题","url":"/2023/12/01/SQL%E9%A2%98/%E7%89%9B%E5%AE%A2/Sql%E9%A2%98/","content":"SQL212\n获取当前薪水第二多的员工的emp_no以及其对应的薪水salary\n描述\n有一个员工表employees简况如下:\n\n\n\n\nemp_no\nbirth_date\nfirst_name\nlast_name\ngender\nhire_date\n\n\n\n\n10001\n1953-09-02\nGeorgi\nFacello\nM\n1986-06-26\n\n\n10002\n1964-06-02\nBezalel\nSimmel\nF\n1985-11-21\n\n\n10003\n1959-12-03\nParto\nBamford\nM\n1986-08-28\n\n\n10004\n1954-05-01\nChirstian\nKoblick\nM\n1986-12-01\n\n\n\n有一个薪水表salaries简况如下:\n\n\n\nemp_no\nsalary\nfrom_date\nto_date\n\n\n\n\n10001\n88958\n2002-06-26\n9999-01-01\n\n\n10002\n72527\n2001-08-02\n9999-01-01\n\n\n10003\n43311\n2001-12-01\n9999-01-01\n\n\n10004\n74057\n2001-11-27\n9999-01-01\n\n\n\n请你查找薪水排名第二多的员工编号emp_no、薪水salary、last_name以及first_name,不能使用order\nby完成,以上例子输出为:\n(温馨提示:sqlite通过的代码不一定能通过mysql,因为SQL语法规定,使用聚合函数时,select子句中一般只能存在以下三种元素:常数、聚合函数,group\nby 指定的列名。如果使用非group by的列名,sqlite的结果和mysql\n可能不一样)\n\n\n\nemp_no\nsalary\nlast_name\nfirst_name\n\n\n\n\n10004\n74057\nKoblick\nChirstian\n\n\n\n解析\n这道题不允许使用order by进行排序,因此需要采用另一种方法,可以使用自连接的方法。\nselect s1.salaryfrom salaries s1 join salaries s2 on s1.salary <= s2.salarygroup by s1.emp_nohaving count(distinct s2.salary) = 2\n自连接,然后连接条件是s1.salary <= s2.salary,思考一下连接过程,左表是驱动表,左表s1从表中取出一行数据,然后依次和右表s2进行比对,如果s1.salary\n小于等于\ns2.salary,则满足条件,遍历完右表后,可以找到所有满足条件的数据,然后和s1表的这行数据存到虚拟表中,这个一行的匹配过程可以看作是后续的group by操作,当一遍遍历完右表s2后,如果右表匹配的不重复数据有一个,那说明左表数据是最大的,因为有等于条件,右表必定有一个数据满足,而其他数据都不满足,所以这个是最大的。如果右表匹配的有两个,那说明右表有两个数据大于等于当前数据,而有一个是他自己本身,所以当前数据是第二大的。因此对s1.salary进行分组后然后判断右边的不重复数据的个数即可找到排名。\nSQL229\n批量插入数据,不适用replace操作\n描述\n题目已经先执行了如下语句:\ndrop table if exists actor; CREATE TABLE actor ( actor_id smallint(5) NOT NULL PRIMARY KEY, first_name varchar(45) NOT NULL, last_name varchar(45) NOT NULL, last_update DATETIME NOT NULL); insert into actor values ('3', 'WD', 'GUINESS', '2006-02-15 12:34:33');\n对于表actor插入如下数据,如果数据已经存在,请忽略(不支持使用replace操作)\n\n\n\nactor_id\nfirst_name\nlast_name\nlast_update\n\n\n\n\n'3'\n'ED'\n'CHASE'\n'2006-02-15 12:34:33'\n\n\n\n解析\n# mysql中常用的三种插入数据的语句: # insert into表示插入数据,数据库会检查主键,如果出现重复会报错; # replace into表示插入替换数据,需求表中有PrimaryKey,# 或者unique索引,如果数据库已经存在数据,则用新数据替换,如果没有数据效果则和insert into一样; # insert ignore表示,如果中已经存在相同的记录,则忽略当前新数据;insert ignore into actor values(\"3\",\"ED\",\"CHASE\",\"2006-02-15 12:34:33\");\nSQL230 创建一个actor_name表\n描述\n对于如下表actor,其对应的数据为:\n\n\n\nactor_id\nfirst_name\nlast_name\nlast_update\n\n\n\n\n1\nPENELOPE\nGUINESS\n2006-02-15 12:34:33\n\n\n2\nNICK\nWAHLBERG\n2006-02-15 12:34:33\n\n\n\n请你创建一个actor_name表,并且将actor表中的所有first_name以及last_name导入该表.\nactor_name表结构如下,题目最后会查询actor_name表里面的数据来对比结果输出:\n\n\n\n列表\n类型\n是否为NULL\n含义\n\n\n\n\nfirst_name\nvarchar(45)\nnot null\n名字\n\n\nlast_name\nvarchar(45)\nnot null\n姓氏\n\n\n\n解析\n考察创建表的三种方式\n1. 常规创建create table if not exists 目标表2. 复制表格create 目标表 like 来源表3. 将table1的部分拿来创建table2create table if not exists actor_name(first_name varchar(45) not null,last_name varchar(45) not null)select first_name,last_namefrom actor\nSQL231 创建索引\n针对如下表actor结构创建索引:\n(注:在 SQLite 中,除了重命名表和在已有的表中添加列,ALTER TABLE\n命令不支持其他操作,\nmysql支持ALTER TABLE创建索引)\nCREATE TABLE actor ( actor_id smallint(5) NOT NULL PRIMARY KEY, first_name varchar(45) NOT NULL, last_name varchar(45) NOT NULL, last_update datetime NOT NULL);\n对first_name创建唯一索引uniq_idx_firstname,对last_name创建普通索引idx_lastname\n解析\n\n添加主键\n\nALTER TABLE tbl_name ADD PRIMARY KEY (col_list);// 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。\n\n添加唯一索引\n\nALTER TABLE tbl_name ADD UNIQUE index_name (col_list);// 这条语句创建索引的值必须是唯一的。\n\n添加普通索引\n\nALTER TABLE tbl_name ADD INDEX index_name (col_list);// 添加普通索引,索引值可出现多次。\n\n添加全文索引\n\nALTER TABLE tbl_name ADD FULLTEXT index_name (col_list);// 该语句指定了索引为 FULLTEXT ,用于全文索引。\n\n删除索引的语法:\n\nDROP INDEX index_name ON tbl_name;// 或者ALTER TABLE tbl_name DROP INDEX index_name;ALTER TABLE tbl_name DROP PRIMARY KEY;\n另一种创建索引的方法,使用create,但是这种方式不能创建主键\nCREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name ON tbl_name (col_name);//该语句指定了索引可以是唯一索引、全文索引、空间索引(一种基于空间对象的空间关系排列的数据结构,比如我们常见的地图定位应用,有兴趣的朋友可以自行谷歌一下)以及普通索引。\nSQL163\n每篇文章同一时刻最大在看人数\n用户行为日志表tb_user_log\n\n\n\n\n\n\n\n\n\n\n\nid\nuid\nartical_id\nin_time\nout_time\nsign_cin\n\n\n\n\n1\n101\n9001\n2021-11-01 10:00:00\n2021-11-01 10:00:11\n0\n\n\n2\n102\n9001\n2021-11-01 10:00:09\n2021-11-01 10:00:38\n0\n\n\n3\n103\n9001\n2021-11-01 10:00:28\n2021-11-01 10:00:58\n0\n\n\n4\n104\n9002\n2021-11-01 11:00:45\n2021-11-01 11:01:11\n0\n\n\n5\n105\n9001\n2021-11-01 10:00:51\n2021-11-01 10:00:59\n0\n\n\n6\n106\n9002\n2021-11-01 11:00:55\n2021-11-01 11:01:24\n0\n\n\n7\n107\n9001\n2021-11-01 10:00:01\n2021-11-01 10:01:50\n0\n\n\n\n(uid-用户ID, artical_id-文章ID, in_time-进入时间, out_time-离开时间,\nsign_in-是否签到)\n场景逻辑说明:artical_id-文章ID代表用户浏览的文章的ID,artical_id-文章ID为0表示用户在非文章内容页(比如App内的列表页、活动页等)。\n问题:统计每篇文章同一时刻最大在看人数,如果同一时刻有进入也有离开时,先记录用户数增加再记录减少,结果按最大人数降序。\n输出示例:\n示例数据的输出结果如下\n\n\n\nartical_id\nmax_uv\n\n\n\n\n9001\n3\n\n\n9002\n2\n\n\n\n解释:10点0分10秒时,有3个用户正在浏览文章9001;11点01分0秒时,有2个用户正在浏览文章9002。\n解析(计算瞬时的最大计数)\n本题难点在于如何计算瞬时的最大计数(在看人数)。\n通常想到的方法是编码+联立。首先对原表的in_time和out_time进行编码,如果是in_time则说明观看人数+1,如果是out_time则说明观看人数-1,分别进行编码,然后合并。\nselect artical_id, in_time dt, 1 difffrom tb_user_logwhere artical_id <> 0union allselect artical_id, out_time dt, -1 difffrom tb_user_logwhere artical_id <>0\n这三个字段表示artical_id文章在dt这个时刻的变化人数为diff。\n然后对这个表使用窗口函数sum求和即可求得每一个时刻的观看人数。\nselect artical_id, sum(diff) over (partition by artical_id order by dt, diff desc) viewer_cntfrom ( select artical_id, in_time dt, 1 diff from tb_user_log where artical_id <> 0 union all select artical_id, out_time dt, -1 diff from tb_user_log where artical_id <>0) t\n这里排序先是使用dt升序,按照时间戳的顺序,然后使用diff降序。因为题目中说如果同一时刻有进入和离开,则先增加再减少,也就是说如果排序时dt相同,则按照diff降序排列。\n得到每个时刻的观看人数后,需要求观看人数最多的时刻的人数。因此最后再按照artical_id分组,求max(viewer_cnt)即可。\nselect artical_id, max(viewer_cnt) max_uvfrom ( select artical_id, sum(diff) over (partition by artical_id order by dt, diff desc) viewer_cnt from ( select artical_id, in_time dt, 1 diff from tb_user_log where artical_id <> 0 union all select artical_id, out_time dt, -1 diff from tb_user_log where artical_id <>0 ) t) ttgroup by artical_idorder by max_uv desc\n本题解参考了题解。\nSQL164\n2021年11月每天新用户的次日留存率\n用户行为日志表tb_user_log\n\n\n\n\n\n\n\n\n\n\n\nid\nuid\nartical_id\nin_time\nout_time\nsign_cin\n\n\n\n\n1\n101\n0\n2021-11-01 10:00:00\n2021-11-01 10:00:42\n1\n\n\n2\n102\n9001\n2021-11-01 10:00:00\n2021-11-01 10:00:09\n0\n\n\n3\n103\n9001\n2021-11-01 10:00:01\n2021-11-01 10:01:50\n0\n\n\n4\n101\n9002\n2021-11-02 10:00:09\n2021-11-02 10:00:28\n0\n\n\n5\n103\n9002\n2021-11-02 10:00:51\n2021-11-02 10:00:59\n0\n\n\n6\n104\n9001\n2021-11-02 11:00:28\n2021-11-02 11:01:24\n0\n\n\n7\n101\n9003\n2021-11-03 11:00:55\n2021-11-03 11:01:24\n0\n\n\n8\n104\n9003\n2021-11-03 11:00:45\n2021-11-03 11:00:55\n0\n\n\n9\n105\n9003\n2021-11-03 11:00:53\n2021-11-03 11:00:59\n0\n\n\n10\n101\n9002\n2021-11-04 11:00:55\n2021-11-04 11:00:59\n0\n\n\n\n(uid-用户ID, artical_id-文章ID, in_time-进入时间, out_time-离开时间,\nsign_in-是否签到)\n问题:统计2021年11月每天新用户的次日留存率(保留2位小数)\n注:\n\n次日留存率为当天新增的用户数中第二天又活跃了的用户数占比。\n如果in_time-进入时间和out_time-离开时间跨天了,在两天里都记为该用户活跃过,结果按日期升序。\n\n输出示例:\n示例数据的输出结果如下\n\n\n\ndt\nuv_left_rate\n\n\n\n\n2021-11-01\n0.67\n\n\n2021-11-02\n1.00\n\n\n2021-11-03\n0.00\n\n\n\n解释:\n11.01有3个用户活跃101、102、103,均为新用户,在11.02只有101、103两个又活跃了,因此11.01的次日留存率为0.67;\n11.02有104一位新用户,在11.03又活跃了,因此11.02的次日留存率为1.00;\n11.03有105一位新用户,在11.04未活跃,因此11.03的次日留存率为0.00;\n11.04没有新用户,不输出。\n解析\n本题针对的是新用户的次日留存率。因此应该首先查出每个用户的最早登陆日期,也就是新用户的第一次登陆日期,这样可以找到每个日期的注册用户。\n然后再查询出每天都有哪些用户登录,将这两个表连接,连接条件是相同用户和用户最早登录日期的下一天仍然登录。如果下一天仍然登录,则该值不为null否则为null\nselect t1.dt, round(count(t2.dt) / count(t1.dt), 2) uv_left_ratefrom ( select uid, min(date(in_time)) dt from tb_user_log group by uid) t1 -- 查询新用户和新用户的最早登录日期left join ( select uid, date(in_time) dt from tb_user_log union select uid, date(out_time) dt from tb_user_log) t2 -- 查询每天登录的用户,如果跨日期,则认为两天都登录了,所以需要查询两次然后合并on t1.uid = t2.uidand datediff(t2.dt, t1.dt) = 1where date_format(t1.dt, '%Y-%m') = '2021-11'group by t1.dtorder by t1.dt\nSQL167 连续签到领金币\n用户行为日志表tb_user_log\n\n\n\n\n\n\n\n\n\n\n\nid\nuid\nartical_id\nin_time\nout_time\nsign_in\n\n\n\n\n1\n101\n0\n2021-07-07 10:00:00\n2021-07-07 10:00:09\n1\n\n\n2\n101\n0\n2021-07-08 10:00:00\n2021-07-08 10:00:09\n1\n\n\n3\n101\n0\n2021-07-09 10:00:00\n2021-07-09 10:00:42\n1\n\n\n4\n101\n0\n2021-07-10 10:00:00\n2021-07-10 10:00:09\n1\n\n\n5\n101\n0\n2021-07-11 23:59:55\n2021-07-11 23:59:59\n1\n\n\n6\n101\n0\n2021-07-12 10:00:28\n2021-07-12 10:00:50\n1\n\n\n7\n101\n0\n2021-07-13 10:00:28\n2021-07-13 10:00:50\n1\n\n\n8\n102\n0\n2021-10-01 10:00:28\n2021-10-01 10:00:50\n1\n\n\n9\n102\n0\n2021-10-02 10:00:01\n2021-10-02 10:01:50\n1\n\n\n10\n102\n0\n2021-10-03 10:00:55\n2021-10-03 11:00:59\n1\n\n\n11\n102\n0\n2021-10-04 10:00:45\n2021-10-04 11:00:55\n0\n\n\n12\n102\n0\n2021-10-05 10:00:53\n2021-10-05 11:00:59\n1\n\n\n13\n102\n0\n2021-10-06 10:00:45\n2021-10-06 11:00:55\n1\n\n\n\n(uid-用户ID, artical_id-文章ID, in_time-进入时间, out_time-离开时间,\nsign_in-是否签到)\n场景逻辑说明:\n\nartical_id-文章ID代表用户浏览的文章的ID,特殊情况artical_id-文章ID为0表示用户在非文章内容页(比如App内的列表页、活动页等)。注意:只有artical_id为0时sign_in值才有效。\n从2021年7月7日0点开始,用户每天签到可以领1金币,并可以开始累积签到天数,连续签到的第3、7天分别可额外领2、6金币。\n每连续签到7天后重新累积签到天数(即重置签到天数:连续第8天签到时记为新的一轮签到的第一天,领1金币)\n\n问题:计算每个用户2021年7月以来每月获得的金币数(该活动到10月底结束,11月1日开始的签到不再获得金币)。结果按月份、ID升序排序。\n注:如果签到记录的in_time-进入时间和out_time-离开时间跨天了,也只记作in_time对应的日期签到了。\n输出示例:\n示例数据的输出结果如下:\n\n\n\nuid\nmonth\ncoin\n\n\n\n\n101\n202107\n15\n\n\n102\n202110\n7\n\n\n\n解释:\n101在活动期内连续签到了7天,因此获得1*7+2+6=15金币;\n102在10.01~10.03连续签到3天获得5金币\n10.04断签了,10.05~10.06连续签到2天获得2金币,共得到7金币。\n解析\n本题最主要的问题是如何判断签到日期是否连续,因此可以联想到连续问题。连续问题通常解决方法是新建一个连续的列,相减得到一个常数值,通过这个相同的值判断连续情况。也就是连续n问题。\nwith t_sign_info as ( select distinct uid , date(in_time) sign_dt , date(in_time) - row_number() over (partition by uid order by date(in_time)) row_num from tb_user_log where artical_id = 0 and sign_in = 1 and datediff(in_time, '2021-07-07') >= 0 and datediff(in_time, '2021-10-31') <= 0)select uid, date_format(sign_dt, '%Y%m') month, sum(coin) coinfrom ( select uid , sign_dt , case row_id % 7 when 3 then 3 when 0 then 7 else 1 end coin from ( select *, row_number() over (partition by row_num, uid order by sign_dt) row_id from t_sign_info ) t) ttgroup by uid, monthorder by month, uid\n首先找到每个用户的合法的签到日期,然后按照用户号分组使用窗口函数分配序号。然后使用签到日期和分配的序号相减,如果签到日期是连续的,那么和序号相减得到的结果的值是固定的,如果不同了,说明日期发生了断连。\n然后再次使用窗口函数分配序号,这样可以看到用户签到的天数情况。\nSQL171\n零食类商品中复购率top3高的商品\n商品信息表tb_product_info\n\n\n\n\n\n\n\n\n\n\n\n\nid\nproduct_id\nshop_id\ntag\nint_\nquantity\nrelease_time\n\n\n\n\n1\n8001\n901\n零食\n60\n1000\n2020-01-01 10:00:00\n\n\n2\n8002\n901\n零食\n140\n500\n2020-01-01 10:00:00\n\n\n3\n8003\n901\n零食\n160\n500\n2020-01-01 10:00:00\n\n\n\n(product_id-商品ID, shop_id-店铺ID, tag-商品类别标签,\nin_price-进货价格, quantity-进货数量, release_time-上架时间)\n订单总表tb_order_overall\n\n\n\n\n\n\n\n\n\n\n\n\nid\norder_id\nuid\nevent_time\ntotal_amount\ntotal_cnt\nstatus\n\n\n\n\n1\n301001\n101\n2021-09-30 10:00:00\n140\n1\n1\n\n\n2\n301002\n102\n2021-10-01 11:00:00\n235\n2\n1\n\n\n3\n301011\n102\n2021-10-31 11:00:00\n250\n2\n1\n\n\n4\n301003\n101\n2021-10-02 10:00:00\n300\n2\n1\n\n\n5\n301013\n105\n2021-10-02 10:00:00\n300\n2\n1\n\n\n6\n301005\n104\n2021-10-03 10:00:00\n170\n1\n1\n\n\n\n(order_id-订单号, uid-用户ID, event_time-下单时间,\ntotal_amount-订单总金额, total_cnt-订单商品总件数, status-订单状态)\n订单明细表tb_order_detail\n\n\n\nid\norder_id\nproduct_id\nprice\ncnt\n\n\n\n\n1\n301001\n8002\n150\n1\n\n\n2\n301011\n8003\n200\n1\n\n\n3\n301011\n8001\n80\n1\n\n\n4\n301002\n8001\n85\n1\n\n\n5\n301002\n8003\n180\n1\n\n\n6\n301003\n8002\n140\n1\n\n\n7\n301003\n8003\n180\n1\n\n\n8\n301013\n8002\n140\n2\n\n\n9\n301005\n8003\n180\n1\n\n\n\n(order_id-订单号, product_id-商品ID, price-商品单价,\ncnt-下单数量)\n场景逻辑说明:\n\n用户将购物车中多件商品一起下单时,订单总表会生成一个订单(但此时未付款,\nstatus-订单状态-\n订单状态为0表示待付款),在订单明细表生成该订单中每个商品的信息;\n当用户支付完成时,在订单总表修改对应订单记录的status-订单状态-\n订单状态为1表示已付款;\n若用户退货退款,在订单总表生成一条交易总金额为负值的记录(表示退款金额,订单号为退款单号,订单状态为2表示已退款)。\n\n问题:请统计零食类商品中复购率top3高的商品。\n注:复购率指用户在一段时间内对某商品的重复购买比例,复购率越大,则反映出消费者对品牌的忠诚度就越高,也叫回头率\n此处我们定义:某商品复购率 = 近90天内购买它至少两次的人数 ÷\n购买它的总人数\n近90天指包含最大日期(记为当天)在内的近90天。结果中复购率保留3位小数,并按复购率倒序、商品ID升序排序\n输出示例:\n示例数据的输出结果如下:\n\n\n\nproduct_id\nrepurchase_rate\n\n\n\n\n8001\n1.000\n\n\n8002\n0.500\n\n\n8003\n0.333\n\n\n\n解释:\n商品8001、8002、8003都是零食类商品,8001只被用户102购买了两次,复购率1.000;\n商品8002被101购买了两次,被105购买了1次,复购率0.500;\n商品8003被102购买两次,被101和105各购买1次,复购率为0.333。\n解析\n本题难点在于找到复购的商品的人数。\n首先看复购的定义,指的是一个用户在某段时间内对一个商品重复购买。也就是说在表中的记录里,用户复购一个商品时的uid和product_id是相同的,因为用户相同、商品相同。因此可以首先使用uid, product_id进行分组,找到某用户对对某商品的购买次数,可以通过event_time判断购买了多少次。\nselect product_id , round(avg(if_repurchase), 3) repurchase_ratefrom ( select info.product_id , if(count(overall.event_time) > 1, 1, 0) if_repurchase from tb_product_info info inner join tb_order_detail detail on info.product_id = detail.product_id inner join tb_order_overall overall on detail.order_id = overall.order_id where info.tag = '零食' and datediff((select max(event_time) from tb_order_overall), overall.event_time) < 90 group by info.product_id, overall.uid) tgroup by product_idorder by repurchase_rate desc, product_id asclimit 3\nSQL173\n店铺901国庆期间的7日动销率和滞销率\n商品信息表tb_product_info\n\n\n\n\n\n\n\n\n\n\n\n\nid\nproduct_id\nshop_id\ntag\nint_\nquantity\nrelease_time\n\n\n\n\n1\n8001\n901\n日用\n60\n1000\n2020-01-01 10:00:00\n\n\n2\n8002\n901\n零食\n140\n500\n2020-01-01 10:00:00\n\n\n3\n8003\n901\n零食\n160\n500\n2020-01-01 10:00:00\n\n\n\n(product_id-商品ID, shop_id-店铺ID, tag-商品类别标签,\nin_price-进货价格, quantity-进货数量, release_time-上架时间)\n订单总表tb_order_overall\n\n\n\n\n\n\n\n\n\n\n\n\nid\norder_id\nuid\nevent_time\ntotal_amount\ntotal_cnt\nstatus\n\n\n\n\n1\n301004\n102\n2021-09-30 10:00:00\n170\n1\n1\n\n\n2\n301005\n104\n2021-10-01 10:00:00\n160\n1\n1\n\n\n3\n301003\n101\n2021-10-02 10:00:00\n300\n2\n1\n\n\n4\n301002\n102\n2021-10-03 11:00:00\n235\n2\n1\n\n\n\n(order_id-订单号, uid-用户ID, event_time-下单时间,\ntotal_amount-订单总金额, total_cnt-订单商品总件数, status-订单状态)\n订单明细表tb_order_detail\n\n\n\nid\norder_id\nproduct_id\nprice\ncnt\n\n\n\n\n1\n301004\n8002\n180\n1\n\n\n2\n301005\n8002\n170\n1\n\n\n3\n301002\n8001\n85\n1\n\n\n4\n301002\n8003\n180\n1\n\n\n5\n301003\n8002\n150\n1\n\n\n6\n301003\n8003\n180\n1\n\n\n\n(order_id-订单号, product_id-商品ID, price-商品单价,\ncnt-下单数量)\n问题:请计算店铺901在2021年国庆头3天的7日动销率和滞销率,结果保留3位小数,按日期升序排序。\n注:\n\n动销率定义为店铺中一段时间内有销量的商品占当前已上架总商品数的比例(有销量的商品/已上架总商品数)。\n滞销率定义为店铺中一段时间内没有销量的商品占当前已上架总商品数的比例。(没有销量的商品/已上架总商品数)。\n只要当天任一店铺有任何商品的销量就输出该天的结果,即使店铺901当天的动销率为0。\n\n输出示例:\n示例数据的输出结果如下:\n\n\n\ndt\nsale_rate\nunsale_rate\n\n\n\n\n2021-10-01\n0.333\n0.667\n\n\n2021-10-02\n0.667\n0.333\n\n\n2021-10-03\n1.000\n0.000\n\n\n\n解释:\n10月1日的近7日(9月25日---10月1日)店铺901有销量的商品有8002,截止当天在售商品数为3,动销率为0.333,滞销率为0.667;\n10月2日的近7日(9月26日---10月2日)店铺901有销量的商品有8002、8003,截止当天在售商品数为3,动销率为0.667,滞销率为0.333;\n10月3日的近7日(9月27日---10月3日)店铺901有销量的商品有8002、8003、8001,截止当天店铺901在售商品数为3,动销率为1.000,\n滞销率为0.000;\n解析\n总体思路先求这三天每天的上架商品总数,然后再求七天内的售出商品总数。\n要计算过去七天的,首先想到使用窗口函数,但是因为售出日期不是连续的,使用窗口函数不能解决问题,因此可以采用连接方式,在连接条件上判断。\n首先求出要求的三天的日期,题目要求只要当天有商品卖出就要展示当天的结果,另一个意思是如果一天没有任何商品售出就不展示该天。所以先根据订单求出需要展示哪些天。\nSELECT DATE(event_time) dtFROM tb_order_overallWHERE DATE(event_time) BETWEEN '2021-10-01' AND '2021-10-03'\n然后求出这几天的每天的上架的商品数量,题目给的条件里没有说明下架日期,默认商品不会下架,这里根据上架日期小于题目中要求的三天即可,使用连接的方法即可求出。\n-- t_date为上一次求的三天日期SELECT dt, COUNT(DISTINCT product_id) cntFROM tb_product_infoINNER JOIN t_dateON DATEDIFF(dt, release_time) >= 0WHERE shop_id = 901GROUP BY dt \n接下来求出七天内的出售的商品,对三个表进行连接,根据要求的店铺id和日期,即可求得每天出售的商品id。注:这里不能求每天出售的商品数,题目中要求的是七天内售出的不同的商品数,如果两天售出同样的商品,正确的结果为1,而如果求每天出售的商品数,结果则大于1了\nSELECT DATE(oo.event_time) dt, od.product_id FROM tb_order_detail odINNER JOIN tb_order_overall ooON od.order_id = oo.order_idINNER JOIN tb_product_info piON od.product_id = pi.product_idWHERE pi.shop_id = 901AND DATE(oo.event_time) BETWEEN '2021-09-25' AND '2021-10-03'\n最后对上架商品表和出售商品表连接,求出结果。\nSELECT t_onsale.dt, ROUND(COUNT(DISTINCT t_sold.product_id) / t_onsale.cnt, 3) sale_rate, ROUND(1 - COUNT(DISTINCT t_sold.product_id) / t_onsale.cnt, 3) unsale_rateFROM t_onsaleLEFT JOIN t_soldON DATEDIFF(t_onsale.dt, t_sold.dt) BETWEEN 0 AND 6GROUP BY t_onsale.dtORDER BY t_onsale.dt\nSQL 165\n统计活跃间隔对用户分级结果\n用户行为日志表tb_user_log\n\n\n\n\n\n\n\n\n\n\n\nid\nuid\nartical_id\nin_time\nout_time\nsign_cin\n\n\n\n\n1\n109\n9001\n2021-08-31 10:00:00\n2021-08-31 10:00:09\n0\n\n\n2\n109\n9002\n2021-11-04 11:00:55\n2021-11-04 11:00:59\n0\n\n\n3\n108\n9001\n2021-09-01 10:00:01\n2021-09-01 10:01:50\n0\n\n\n4\n108\n9001\n2021-11-03 10:00:01\n2021-11-03 10:01:50\n0\n\n\n5\n104\n9001\n2021-11-02 10:00:28\n2021-11-02 10:00:50\n0\n\n\n6\n104\n9003\n2021-09-03 11:00:45\n2021-09-03 11:00:55\n0\n\n\n7\n105\n9003\n2021-11-03 11:00:53\n2021-11-03 11:00:59\n0\n\n\n8\n102\n9001\n2021-10-30 10:00:00\n2021-10-30 10:00:09\n0\n\n\n9\n103\n9001\n2021-10-21 10:00:00\n2021-10-21 10:00:09\n0\n\n\n10\n101\n0\n2021-10-01 10:00:00\n2021-10-01 10:00:42\n1\n\n\n\n(uid-用户ID, artical_id-文章ID, in_time-进入时间, out_time-离开时间,\nsign_in-是否签到)\n问题:统计活跃间隔对用户分级后,各活跃等级用户占比,结果保留两位小数,且按占比降序排序。\n注:\n\n用户等级标准简化为:忠实用户(近7天活跃过且非新晋用户)、新晋用户(近7天新增)、沉睡用户(近7天未活跃但更早前活跃过)、流失用户(近30天未活跃但更早前活跃过)。\n假设今天就是数据中所有日期的最大值。\n近7天表示包含当天T的近7天,即闭区间[T-6, T]。\n\n输出示例:\n示例数据的输出结果如下\n\n\n\nuser_grade\nratio\n\n\n\n\n忠实用户\n0.43\n\n\n新晋用户\n0.29\n\n\n沉睡用户\n0.14\n\n\n流失用户\n0.14\n\n\n\n解释:\n今天日期为2021.11.04,根据用户分级标准,用户行为日志表tb_user_log中忠实用户有:109、108、104;新晋用户有105、102;沉睡用户有103;流失用户有101;共7个用户,因此他们的比例分别为0.43、0.29、0.14、0.14。\n解析\n本题主要判断的是用户的最后一次活跃、第一次活跃和所有活跃日期的最大值的比较。\n所以首先应该查找出所有用户的首次活跃和最后一次活跃日期,这里可以采用分组的方法,每一组里的最大值和最小值即是结果。\n所有日期的最大值可以通过再次查找表取所有数据的最大值,但是这样会重复查找。可以在刚刚查找每个用户的活跃日期时顺便查找出所有用户的最后活跃日期。即在分组后使用一次窗口函数,求分组后的结果的最大值,即为全局的最大值。\nSELECT uid, MAX(out_time) last_dt, MIN(out_time) first_dt, MAX(MAX(out_time)) OVER () todayFROM tb_user_logGROUP BY uid\n这里提供了一个思路:如果想在分组后求全局的聚合结果,可以再次使用窗口函数进行求解。\n所以本题解法为:\nWITH t_all AS ( SELECT uid, MAX(out_time) last_dt, MIN(out_time) first_dt, MAX(MAX(out_time)) OVER () today FROM tb_user_log GROUP BY uid),t_grade AS ( SELECT uid, CASE WHEN DATEDIFF(today, last_dt) <= 6 AND DATEDIFF(today, first_dt) > 6 THEN '忠实用户' WHEN DATEDIFF(today, first_dt) <= 6 THEN '新晋用户' WHEN DATEDIFF(today, last_dt) > 29 THEN '流失用户' ELSE '沉睡用户' END user_grade FROM t_all)SELECT user_grade, ROUND(COUNT(uid) / SUM(COUNT(user_grade)) OVER (), 2) ratioFROM t_gradeGROUP BY user_gradeORDER BY ratio DESC\n同样的思路,最后需要判断所占比率,每类用户的数量除以所有用户的数量,已经按照用户等级分组了,对分组后的结果使用窗口函数对每个组的行数进行求和,就得到了总数。\nSQL 170\n某店铺的各商品毛利率及店铺整体毛利率\n描述\n商品信息表tb_product_info\n\n\n\n\n\n\n\n\n\n\n\n\nid\nproduct_id\nshop_id\ntag\nin_price\nquantity\nrelease_time\n\n\n\n\n1\n8001\n901\n家电\n6000\n100\n2020-01-01 10:00:00\n\n\n2\n8002\n902\n家电\n12000\n50\n2020-01-01 10:00:00\n\n\n3\n8003\n901\n3C数码\n12000\n50\n2020-01-01 10:00:00\n\n\n\n(product_id-商品ID, shop_id-店铺ID, tag-商品类别标签,\nin_price-进货价格, quantity-进货数量, release_time-上架时间)\n订单总表tb_order_overall\n\n\n\n\n\n\n\n\n\n\n\n\nid\norder_id\nuid\nevent_time\ntotal_amount\ntotal_cnt\nstatus\n\n\n\n\n1\n301001\n101\n2021-10-01 10:00:00\n30000\n3\n1\n\n\n2\n301002\n102\n2021-10-01 11:00:00\n23900\n2\n1\n\n\n3\n301003\n103\n2021-10-02 10:00:00\n31000\n2\n1\n\n\n\n(order_id-订单号, uid-用户ID, event_time-下单时间,\ntotal_amount-订单总金额, total_cnt-订单商品总件数, status-订单状态)\n订单明细表tb_order_detail\n\n\n\nid\norder_id\nproduct_id\nprice\ncnt\n\n\n\n\n1\n301001\n8001\n8500\n2\n\n\n2\n301001\n8002\n15000\n1\n\n\n3\n301002\n8001\n8500\n1\n\n\n4\n301002\n8002\n16000\n1\n\n\n5\n301003\n8002\n14000\n1\n\n\n6\n301003\n8003\n18000\n1\n\n\n\n(order_id-订单号, product_id-商品ID, price-商品单价,\ncnt-下单数量)\n场景逻辑说明:\n\n用户将购物车中多件商品一起下单时,订单总表会生成一个订单(但此时未付款,status-订单状态为0表示待付款),在订单明细表生成该订单中每个商品的信息;\n当用户支付完成时,在订单总表修改对应订单记录的status-订单状态为1表示已付款;\n若用户退货退款,在订单总表生成一条交易总金额为负值的记录(表示退款金额,订单号为退款单号,status-订单状态为2表示已退款)。\n\n问题:请计算2021年10月以来店铺901中商品毛利率大于24.9%的商品信息及店铺整体毛利率。\n注:商品毛利率=(1-进价/平均单件售价)*100%;\n店铺毛利率=(1-总进价成本/总销售收入)*100%。\n结果先输出店铺毛利率,再按商品ID升序输出各商品毛利率,均保留1位小数。\n输出示例:\n示例数据的输出结果如下:\n\n\n\nproduct_id\nprofit_rate\n\n\n\n\n店铺汇总\n31.0%\n\n\n8001\n29.4%\n\n\n8003\n33.3%\n\n\n\n解释:\n店铺901有两件商品8001和8003;8001售出了3件,销售总额为25500,进价总额为18000,毛利率为1-18000/25500=29.4%,8003售出了1件,售价为18000,进价为12000,毛利率为33.3%;\n店铺卖出的这4件商品总销售额为43500,总进价为30000,毛利率为1-30000/43500=31.0%\n解析(WITH ROLLUP)\n题目理解不难,难点在于怎么同时计算出每个商品的毛利率和店铺整体毛利率。\n第一种可以采用分别计算的方法,先计算出店铺的毛利率然后再计算每个商品的毛利率,最后进行合并即可。\n第二种是采用WITH ROLLUP方法,因为整体毛利率和某个商品的毛利率计算方法相似。\n这里需要对商品毛利率的计算方法做一个转换。进价平均单件售价,上下同时乘出售的总数量,就变成了总进价成本该商品总销售收入,所以计算方法和计算整体毛利率相同。\nSELECT product_id, CONCAT(profit_rate, '%') profit_rateFROM ( SELECT COALESCE(product_id, '店铺汇总') product_id, ROUND(100 * (1 - SUM(in_price*cnt) / SUM(price*cnt)), 1) profit_rate FROM ( SELECT info.product_id, info.in_price, detail.price, detail.cnt FROM tb_order_detail detail INNER JOIN tb_product_info info ON detail.product_id = info.product_id INNER JOIN tb_order_overall overall ON detail.order_id = overall.order_id WHERE info.shop_id = 901 AND DATE(overall.event_time) >= '2021-10-01' ) t1 GROUP BY product_id WITH ROLLUP HAVING profit_rate >= 24.9 OR product_id IS NULL ORDER BY product_id) t2\n","categories":["牛客"],"tags":["SQL","牛客"]},{"title":"设计模式课程","url":"/2023/10/23/%E8%AF%BE%E7%A8%8B/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/","content":"\n1 策略模式\n1.1 定义\n策略模式定义:它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化不会影响到使用算法的客户。\n\n1.2 结构图\n\n1.3 代码\nabstract class Strategy { public abstract void AlgorithmInterface();}\nclass ConcreteStrategyA extends Strategy { @override public void AlgorighmInterface() { // 算法A实现 }}class ConcreteStrategyB extends Strategy { @override public void AlgorighmInterface() { // 算法B实现 }}class ConcreteStrategyC extends Strategy { @override public void AlgorighmInterface() { // 算法C实现 }}\nclass Context { private Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } // 根据传入的具体的策略对象调用其算法 public void ContextInterface() { strategy.AlgorighmInterface(); }}\n1.4 总结\n策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。\n策略模式的Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。\n2 装饰模式\n2.1 定义\n装饰模式定义:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更加灵活。\n2.2 结构图\n\n2.3 代码\ninterface Component { void Operation();}\npublic class ConcreteComponent implements Component { @override void Operation() { // 具体的操作 }}\npublic abstract class Decorator implements Component { protected Component component; public void setComponent(Component component) { this.component = component; } @override public void Operation() { if (component != null) { component.operation(); } }}\nclass ConcreteDecoratorA extends Decorator { private String addedState; @override public void Opertaion() { super.opertion(); //对象A的具体操作 addedState = \"new state\"; }}class ConcreteDecoratorB extends Decorator { @override public void Operation() { super.opertaion(); // 对象B的具体操作 AddedBehavior(); } private void AddedBehavior() { }}\npublic static void main() { ConcreteDecoratorA d1 = new ConcreteDecoratorA(); ConcreteDecoratorB d2 = new ConcreteDecoratorB(); // 用d2对象来包装d1 d2.setComponent(d1); // 调用d2的具体操作,最终会也会把包装过的对象的具体操作也执行 d2.Operation();}\n具体实现中可能会与结构图有不同,如直接让Decorator抽象类继承ConcreteComponent类。\n2.4 总结\n装饰模式是为已有功能动态地添加更多功能的一种方式。\n装饰模式提供了一个非常好的解决方案,它把每个要装饰的功能放在单独的类中,并让这个类包装它所要装饰的对象,因此,当需要执行特殊行为时,客户代码就可以在运行时根据需要有选择地、按顺序地使用装饰功能包装对象了。\n3 代理模式\n3.1 定义\n代理模式定义:为其他对象提供一种代理以控制对这个对象的访问。\n3.2 结构图\n\n3.3 代码\npublic abstract class Subject { public abstarct void Request();}\npublic class RealSubject extends Subject { @override public void Request() { // 真实的操作请求 }}\npublic class Proxy extends Subject { RealSubject realSubject; @override public void Request() { if (realSubject == null) { realSubject = new RealSubject(); } realSubject.Request(); }}\n3.4 总结\n\n代理模式通常有如下几钟用途:\n\n\n远程代理\n虚拟代理\n保护代理\n智能指引\n\n\n代理模式的主要优点:\n\n\n代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;\n代理对象可以扩展目标对象的功能;\n代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性;\n\n4 工厂方法模式\n4.1 定义\n工厂方法模式定义:定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。\n4.2 结构图\n\n4.3 代码\ninterface Product {}\nclass ConcreteProduct implements Product {}\ninterface Factory { Product FactoryMethod();}\nclass ConcreteFactory implements Factory { @override public Product FactoryMethod() { // 生成具体对象的方法 Product product = new ConcreteProduct(); return product; }}\n4.4 总结\n\n优点\n\n\n符合开闭原则\n符合单一职责原则\n不使用静态工厂方法,可以形成基于继承的等级结构\n\n5 原型模式\n5.1 定义\n原型模式定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。\n5.2 结构图\n\n5.3 代码\npublic abstract class Prototype { private String id; public abstract Prototype Clone();}\nclass ConcretePrototype extends Prototype { @override public Protytpe Clone() { // 克隆当前对象 return newObject; }}\n5.4 总结\n具体实现时无需创建Prototype接口,直接实现Java中的Cloneable接口即可。同时克隆时注意深拷贝和浅拷贝的问题。\n6 模板方法模式\n6.1 定义\n模板方法模式定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。\n6.2 结构图\n\n6.3 代码\nabstract class TemplateClass { public abstract void PrimitiveOpeartion1(); public abstract void PrimitiveOpeartion2();\tpublic void TemplateMethod() { PrimitiveOpeartion1(); PrimitiveOpeartion2(); // 其他步骤 }}\npublic class ConcreteClass extends TemplateClass { @override public void PrimitiveOperation1() { // 方法1 } @override public void PrimitiveOperation2() { // 方法2 }}\n实现类实现抽象模板中定义的抽象方法。\n6.4 总结\n模板方法模式提供了一个很好的代码复用平台\n。因为有时候,会遇到由一系列步骤构成的过程需要执行。这个过程从高层次上看是相同的,但有些步骤的实现可能不同。这时候,通常就应该要考虑用模板方法模式了。当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。\n7 外观模式\n7.1 定义\n外观模式定义:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得子系统更加容易使用。\n7.2 结构图\n\n7.3 代码\nclass SubSystemOne { public void MethodOne() { // 方法1 }}class SubSystemTwo { public void MethodTwo() { // 方法2 }}\nclass Facade { private SubSystemOne subSystemOne; private SubSystemTwo subSystemTwo; public Facade() { subSystemOne = new SubSystemOne(); subSystemTwo = new SubSystemTwo(); } public void MethodA() { subSystemOne.MethodOne(); subSystemTwo.MethodTwo(); } public void MethodB() { subSystemTwo.MethodTwo(); }}\n7.4 总结\n首先,在设计初期阶段,应该要有意识的将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层和业务逻辑层、业务逻辑层和表示层的层与层之间建立外观Facade,这样可以为复杂的子系统提供一个简单的接口,使得耦合大大降低。其次,在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,大多数的模式使用时也都会产生很多很小的类,这本是好事,但也给外部调用它们的用户程序带来了使用上的困难,增加外观Facade可以提供一个简单的接口,减少它们之间的依赖。第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求开发必须要依赖于它。此时用外观模式Facade也是非常合适的。你可以为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Facade对象交互,Facade与遗留代码交互所有复杂的工作。\n8 建造者模式\n8.1 定义\n建造者模式定义:也叫生成器模式,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。\n8.2 结构图\n\n8.3 代码\nclass Product { List<String> parts = new ArrayList<>(); // 添加产品部件 public void Add(String part) { parts.add(part); } public void Show() { // 展示所有产品部件 }}\ninterface Builder { void BuildPartA(); void BuildPartB(); Product GetResult();}\nclass ConcreteBuilder implements Builder { private Product product = new Product(); @override public void BuilderPartA() { product.Add(\"部件A\"); } @override public void BuilderPartB() { product.Add(\"部件B\"); } @override public Product GetResult() { return product; }}\nclass Director { public void Construct(Builder builder) { builder.BuildPartA(); builder.BuildPartB(); }}\n8.4 总结\n主要是用于创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。\n9 观察者模式\n9.1 定义\n观察者模式:也叫做发布-订阅模式,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生变化时,会通知所有观察者对象,使他们能够自动更新自己。\n9.2 结构图\n\n","tags":["设计模式"]}]