Premium* (Java) - ShuweiLeung/Thinking GitHub Wiki

该页笔记始于"2018.10.7",结于“2018.10.XX”,使用Java语言

心得

  1. 将BST转换为有序序列使用中序遍历
  2. 对于无向图,要判断是否存在环,需要用到Union-find,当两个属于相同集合的结点间存在一条边时,代表存在环

1.(252:Easy)Meeting Rooms

  • 很简单,按start排序后,从前往后开始遍历,只要后面interval的起点大于等于前一个interval的终点就满足条件。

2.(253:Medium)(经典题)Meeting Rooms II

  • 核心思想:我们将开会的开始时间和结束时间按照从小到大排序,然后从前向后同步遍历start时间和end时间,当某一时刻的开会时间比上一时刻的结束时间小,就需要占用一个新的会议室,否则只需要占用刚结束会议的房间即可
  • 个人感觉将start、end单独排序的解法比用PriorityQueue的解法难理解一些。

3.(426:Medium)(非常经典题)Convert Binary Search Tree to Sorted Doubly Linked List

  • 之前我们在做Tree类型的题目时,有一道题要求将BST按结点值大小排序,用到的方法就是中序遍历。在该题中,如果单纯使用中序遍历而不做额外的处理,是可以建立一个单向链表的。但该题要求建立一个双向链表,这就要求我们在每层递归结束后,要向上层返回一个经过当前结点的链表的头尾指针(在实现中用一个长度为2的数组保存)。而在该层获得左右子树的双向链表的头尾指针后,由该层拼接得到一个完整的双向链表片段,将新链表片段的头尾指针返回上层。
  • 注意:最后记得特殊处理链表的头尾指针。
  • 该题是将BST按大小串成一个双向链表,所以肯定涉及到中序遍历,上面自己想的方法很直观且容易理解,但是代码量可能稍多一点,discuss中给的高票答案更简洁,我将其贴在下边。
    //step1: inorder tranversal by recursion to connect the original BST
    //step2: connect the head and tail to make it circular
    //Tips: Using dummy node to handle corner case
    Node prev = null;
    public Node treeToDoublyList(Node root) {
        if (root == null) return null;
        Node dummy = new Node(0, null, null);
        prev = dummy; //pre一开始指向的是dummy node
        helper(root);
        //connect head and tail
        //此时prev指向的是双向链表的末尾,dummy链接的是链表的头部
        prev.right = dummy.right;
        dummy.right.left = prev;
        return dummy.right;
    }

    private void helper (Node cur) {
        //inorder traversal
        if (cur == null) return;
        helper(cur.left);
        prev.right = cur;
        cur.left = prev;
        prev = cur;
        helper(cur.right);
    }

4.(158:Hard)(奇怪题)Read N Characters Given Read4 II - Call multiple times

  • 该题本意是:read4(buffer)函数是从文件中读取4个字符存入buffer中****,read(buffer,n)函数是从文件中读取n个字符存入buffer,但从实现上来讲是从read4导出的临时缓冲区中连续读n个字符出来,所以类似于Queue的数据结构,生产者消费者模型。

第一次调用时,如果read4读出的多余字符我们要先将其暂存起来,这样第二次调用时先读取这些暂存的字符

第二次调用时,如果连暂存字符都没读完,那么这些暂存字符还得留给第三次调用时使用

这些字符满足先进先出,所以我们可以用一个Queue暂存这些字符.

需要用Queue保存之前多读的character。每次读时,先看Queue里的够不够,如果不够,先读到够为止。

  • 注意read(buf,n)也是要将文件中的内容存入buf中的,和read4(buf)一样
  • 具体可以参考代码实现,该题题意感觉表述不清楚

5.(157:Easy)(奇怪题)Read N Characters Given Read4

  • 该题和第158题一样,但需要明确的是:buf和file是两个不一样的东西,是从file里读取内容到buf中去

6.(314:Medium)(非常经典题)Binary Tree Vertical Order Traversal

  • 该题其实有一些dfs的味道,因为我们在使用dfs的时候,可能会用到递归的层次,每一层都是上一层的层次数加1。而对于该题而言,假设当前结点的列数为n,则其左子树根节点的列数为n-1,右子树根节点的列数为n+1。又因为题目要求答案是从上到下,从左至右给出每列结点的,所以要使用BFS而不是DFS(无法保证从上到下)。但在使用BFS的过程中,我们没法用一个队列来记录相应结点的列数信息,所以这里使用的方法是,新建一个colQueue来存储对应位置上的结点列数,colQueue与nodeQueue同时poll同时add以保证一致性。具体可以参看代码实现。

7.(277:Medium)(非常经典题)Find the Celebrity

  • 该题其实有点像有向图的感觉,我们用下面的有向图来说明该问题
A -> B -> C -> D <- E <- F
               ^
               |
               G
               ^
               |
               H
  • 从上图中可以看出,know关系可以看作一个有向图,而celebrity就是出度为0的结点。我们在第一次遍历的过程中,If candidate knows i, then switch candidate,因为candidate只能被别人知道,而不能知道别人。而在第二次遍历的过程中,我们要确定,每个人都知道该candidate,而candidate不知道其他每一个人,就好比G可能不知道D,即G->D边若断开,则D便不是candidate了。

8.(269:Hard)(非常非常经典题)Alien Dictionary

  • 该题一开始以为按字典序排列不仅是水平方向字母的排序而且是垂直方向字母的排序,后来才反应过来,单词的字典序排列只需要考虑"垂直方向"的比较即可。此外,一开始尝试使用赋予不同字符优先级的方式进行处理,但比较复杂,一方面体现在不同单词优先级最小相差的数量不好判断。
  • 正确的思路是使用拓扑排序的方法,我们首先要用一个map存储从不同结点出发组成的有向边,用另一个map来存储每个节点的入度。当垂直方向上有大小关系存在时,如's'<'r',则建立一个s->r的有向边,即将r加入字符s的map中的key列表中,并将r的入度加1。最后,我们从入度为0的字符入手,建立一个字典序,注意题目中说道There may be multiple valid order of letters, return any one of them is fine.
  • 当然,也存在题目所给的字典序排列不符合要求的情况,从技术角度来讲,即存在环,此时,最终返回结果的res字符串长度一定会比真正字符的种类数少,因为环中的结点均没有加入到结果字符串中,此时返回空字符串即可。具体实现可以参看代码及注释。

9.(325:Medium)(非常经典题)Maximum Size Subarray Sum Equals k

  • 该题其实也算是有一些trick的成分,O(n)的方法是用一个变量计算当前遍历元素的累加和,并将当前的累加和及对应元素索引存入map中,每次循环时,均判断map中是否存在sum-k的累加和,存在的话则求该subarray的长度并更新res结果变量。需要注意,当某一时刻循环时sum已存在于map中,我们并不更新map中的对应的value值,因为题目要求最长的subarray,满足当前sum的坐标越小越好
  • 其实该题一开始想用dp来求解,但如果用动态规划的话,为了求子数组的和,一定得设一个二维的dp数组,时间复杂度一定为O(n^2),故放弃,断定该题一定不是动态规划问题,于是才有了上边Hash Table的求解思路。

10.(246:Easy)Strobogrammatic Number

  • 要成为一个符合条件的Strobogrammatic Number,对称的两个数只能为一下情况之一(0,0),(1,1),(8,8),(6,9),(9,6),单个数字只能为0,1,8。将以上情况全部考虑进去,从两边开始向中央夹逼判断即可,注意while循环的终止条件是i<=j,当i=j的时候按单个数字判断。

11.(247:Medium)(非常经典题)Strobogrammatic Number II

  • 该题显然要使用递归进行求解,但与以往的递归方式的不同之处在于,它是夹逼递归,即每一层要将下一层返回的结果进行前后的包夹,将包夹后的结果再返回上一层递归终止的条件是:当当前层次的字符串长度为0时返回一些值;当当前层次的字符串长度为1时,返回一些值。具体请参考代码实现及注释。

12.(161:Medium)(经典题)One Edit Distance

  • 其实我们首先要排除的情况是:1. 当字符串s和t的长度相差大于1时,肯定是不符合条件的,返回false 2. 当字符串s和t完全相同时,相差0 edit distance,返回false 3. 当s和t的长度其中一个是0,另一个是1时,肯定符合条件,返回true
  • 在进行完以上判别后,我们在traverse过程中(指针要同时小于s和t的长度)只需要分三种情况:1. s与t长度相等,replace一个字符 2. s比t长度大,delete一个字符 3. s比t长度小,add一个字符三种情况进行分类讨论,一旦traverse的过程中发现某个字符不对应相等,则按对应的操作进行处理,然后比较更新后的字符串与t是否相同,相同返回true,不相同返回false。
  • 此外,需要注意最后的默认返回值,当s="ab",t="abc"时,我们在traverse的过程中所有字符一定都相同,所以最后默认返回true,因为只要在s字符串末尾添加t的最后一个字符即可。s="abc", t="ab"同理。
  • There're 3 possibilities to satisfy one edit distance apart:
1) Replace 1 char:
   s: a B c
   t: a D c
2) Delete 1 char from s: 
   s: a D  b c
   t: a    b c
3) Delete 1 char from t
   s: a   b c
   t: a D b c
  • Concise code is following:
public boolean isOneEditDistance(String s, String t) {
    for (int i = 0; i < Math.min(s.length(), t.length()); i++) {
	if (s.charAt(i) != t.charAt(i)) {
	    if (s.length() == t.length()) // s has the same length as t, so the only possibility is replacing one char in s and t
	        return s.substring(i + 1).equals(t.substring(i + 1));
	    else if (s.length() < t.length()) // t is longer than s, so the only possibility is deleting one char from t
		return s.substring(i).equals(t.substring(i + 1));
	    else // s is longer than t, so the only possibility is deleting one char from s
		return t.substring(i).equals(s.substring(i + 1));
	}
    }
    //All previous chars are the same, the only possibility is deleting the end char in the longer one of s and t
    return Math.abs(s.length() - t.length()) == 1;
}

13.(408:Easy)(经典题)Valid Word Abbreviation

  • 该题不应求word的所有缩写,而是应该直接用缩写去验证word,记得利用Character.isLetter()Character.isDigit()两个API来方便的进行处理。当处理缩写abbr时,要分别处理数字和字符。

14.(270:Easy)Closest Binary Search Tree Value

  • 直接上代码:
public int closestValue(TreeNode root, double target) {
    int closestVal = root.val;
    while(root != null) {
        if(Math.abs(target - root.val) < 
            Math.abs(target - closestVal)) 
        {
            closestVal = root.val;    
        }
        if(target == root.val) {
            return root.val;
        }
        else if(target < root.val) {
            root = root.left;
        }
        else {
            root = root.right;
        }
    }
    return closestVal;
}

15.(490:Medium)(非常经典题)The Maze

  • 直接贴代码。需要注意,球只有在碰到墙时才会停止,在滚动过程中不能中途停下
    class Point {
        int x,y;
        public Point(int _x, int _y) {x=_x;y=_y;}
    }
    public boolean hasPath(int[][] maze, int[] start, int[] destination) {
        int m=maze.length, n=maze[0].length;
        if (start[0]==destination[0] && start[1]==destination[1]) return true;
        int[][] dir=new int[][] {{-1,0},{0,1},{1,0},{0,-1}}; //运动方向数组
        boolean[][] visited=new boolean[m][n];  //visited矩阵
        LinkedList<Point> list=new LinkedList<>(); //存储当前可达位置
        visited[start[0]][start[1]]=true;
        list.offer(new Point(start[0], start[1]));
        while (!list.isEmpty()) {
            Point p=list.poll();
            int x=p.x, y=p.y;
            for (int i=0;i<4;i++) { //从当前点向上下左右四个方向前进
                int xx=x, yy=y;
                while (xx>=0 && xx<m && yy>=0 && yy<n && maze[xx][yy]==0) {
                    xx+=dir[i][0];
                    yy+=dir[i][1];
                }
                //往后退一步才是真正到达的位置
                xx-=dir[i][0];
                yy-=dir[i][1];
                if (visited[xx][yy]) continue; //如果是之前到达过的位置,跳过
                visited[xx][yy]=true;
                if (xx==destination[0] && yy==destination[1]) return true;
                list.offer(new Point(xx, yy)); //加入list集合中
            }
        }
        return false;
    }

16.(346:Easy)Moving Average from Data Stream

  • 该题其实就是模拟一个滑动窗口,初始化时限定了窗口的大小,当窗口中元素未满时,可以持续添加元素;当窗口满了以后,必须移去最先移进窗口的元素,然后才能添加进新元素。基于以上观察,我们用Queue这个数据结构即可实现上述的要求。

17.(286:Medium)(非常经典题)Walls and Gates

  • 和DFS中的某一题很相似,求各个位置到门的距离,可以转化为从门到各个距离的问题。于是从0位置开始DFS求到各个位置的距离,只要从当前这个门到达当前这个位置的距离比之前的小,就更新当前位置的值。
  • 注意在向四面八方搜索的时候,可能的搜索路径为A->B->A,即有回路,避免这种情况的方法是,当当前的distance距离比该位置的值大,则直接返回。因为以当前的距离做搜索肯定比所有位置的值都大。

18.(266:Easy)Palindrome Permutation

  • 注意字符串中可能有非字母字符。用一个map存储各个字符的词频,然后遍历字典,要组成回文序列,所有字符中只能有一个字符出现的次数为奇数次。

19.(360:Medium)(非常经典题)Sort Transformed Array

  • 题目中的要求让我们在O(n)中实现,那么我们只能另辟蹊径。其实这道题用到了大量的高中所学的关于抛物线的数学知识,我们知道,对于一个方程f(x) = ax2 + bx + c 来说,如果a>0,则抛物线开口朝上,那么两端的值比中间的大,而如果a<0,则抛物线开口朝下,则两端的值比中间的小。而当a=0时,则为直线方法,是单调递增或递减的。那么我们可以利用这个性质来解题,题目中说明了给定数组nums是有序的,如果不是有序的,我想很难有O(n)的解法。正因为输入数组是有序的,我们可以根据a来分情况讨论:
  1. 当a>0,说明两端的值比中间的值大,那么此时我们从结果res后往前填数,用两个指针分别指向nums数组的开头和结尾,指向的两个数就是抛物线两端的数,将它们之中较大的数先存入res的末尾,然后指针向中间移,重复比较过程,直到把res都填满。
  2. 当a<0,说明两端的值比中间的小,那么我们从res的前面往后填,用两个指针分别指向nums数组的开头和结尾,指向的两个数就是抛物线两端的数,将它们之中较小的数先存入res的开头,然后指针向中间移,重复比较过程,直到把res都填满。
  3. 当a=0,函数是单调递增或递减的,那么从前往后填和从后往前填都可以,我们可以将这种情况和a>0合并。

20.(694:Medium)(非常经典题)Number of Distinct Islands

  • 该题判断是否是一个岛屿的方法非常简单,用DFS即可,将已遍历的位置标记为0。该题难点在于,如何判断两个岛屿相同。该题解法中采用的是"相对位置"的方法。对于双重for循环中遇到的某个岛屿的第一个值为1的点,将其相对位置定为(0,0),并在dfs过程中,将路径记录在字符串中,并将该字符串放入一个set中用于判断是否为不同种类的岛屿。
  • 具体实现和思路请参看代码实现和注释。

21.(708:Medium)(经典题)Insert into a Cyclic Sorted List

  • 该题就是一个典型的链表Linked List题目,题目不难,但关键要注意几种特殊情况
  1. insertVal比head值小(应将pointer指向当前环中最大元素的位置)
  2. insertVal是全局最大的元素(将pointer指向当前环中最大元素的位置,在当前环中最大元素的位置后面添加新元素)
  3. insertVal是全局最小的元素(将pointer指向当前环中最大元素的位置,在当前环中最大元素的位置后面添加新元素)
  4. 环中可能有重复元素,当当前环中的元素全部相同时,如3->3->3(用一个全局变量originalHead来记录输入的head位置,在进行while循环时,若pointer.next = originalHead,则跳出循环)

22.(281:Medium)(经典题)Zigzag Iterator

  • 就是设立两个指针间隔的往后读取各个list中的一个元素即可。我这里用order变量来表示下一个应该读取哪个list中的元素,如果order=-1,读第一个list;如果order=1,读第二个list。但是在初始化order时需要注意,如果list1是空的,则order应为1;如果list2是空的,则order=-1。在next()函数向后读取的过程中,判断条件应包括当前list是否为空。

23.(251:Medium)Flatten 2D Vector

  • 用一个list将2D vector中的所有元素按顺序存入即可。

24.(716:Easy)Max Stack

  • 该题用一个堆栈stack大顶堆PriorityQueue进行求解即可。pq的顶端永远是最大的元素。当从stack中删除最大元素时,将最大元素上方的数字存入一个临时堆栈中,待pop出最大元素后,将临时堆栈中的元素再次添加进stack即可。

25.(170:Easy)(经典题)Two Sum III - Data structure design

  • 可以使用Map,key是元素值,value是该值出现的次数,当判断是否有指定的两个元素值之和是value时,直接遍历map的keyset,判断value-key是否存在
  • 当然该题也可以用一个list存储元素,每次新加入元素后都得重新排序,用双指针的思路来查找是否有两元素之和为value,但该思路效率远远低于使用map的思路。

26.(339:Easy)Nested List Weight Sum

  • 该题用DFS求解即可,如果判断它是一个list,就递归并将深度加一;如果判断它是一个integer,就直接乘以此时的深度并加到结果变量上。

27.(261:Medium)(非常经典题)Graph Valid Tree

  • 对于无向图,要判断是否存在环,需要用到Union-find,当两个属于相同集合的结点间存在一条边时,代表存在环。此外为了使所有结点构成一棵树,最后所有结点必须同属一个集合,即root只能有一个,这可以由set集合来存储所有root判断,初始时,set中存储0..n-1个结点,每union一对结点,将附属的结点代表的root从set中删去,最后全部处理完后,判断set的大小是否为1即可。

28.(280:Medium)(经典题)Wiggle Sort

  • 这道题让我们求摆动排序,跟Wiggle Sort II相比起来,这道题的条件宽松很多,只因为多了一个等号。由于等号的存在,当数组中有重复数字存在的情况时,也很容易满足题目的要求
  • 该题有两种方法,一种方法是,将nums数组按升序排列,然后将后边元素移到前面进行wiggle sort的构造,代码及示例如下:
奇数个
nums = [1,2,3,4,5,6,7]
	1,7,3,4,5,6,2
	1,7,3,5,4,6,2

偶数个
nums = [1,2,3,4,5,6]
        1,5,3,4,2,6
public void wiggleSort(int[] nums) {
	Arrays.sort(nums);
	
	int i = 1, j;
	
	if(nums.length % 2 == 0)
		j = nums.length - 2;
	else
		j = nums.length - 1;
	
	while(i < j) {
		int t = nums[i];
		nums[i] = nums[j];
		nums[j] = t;
		
		i += 2;
		j -= 2;
	}
}
  • 另一种方法就是严格按定义进行重排列,不用进行数组排序,所以效率比第一种方法更高,代码如下:
    The final sorted nums needs to satisfy two conditions:

    If i is odd, then nums[i] >= nums[i - 1];

    If i is even, then nums[i] <= nums[i - 1].

    The code is just to fix the orderings of nums that do not satisfy 1
    and 2.

    public void wiggleSort(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            if (i % 2 == 1) {
                if (nums[i] < nums[i - 1]) {
                    int temp = nums[i];
                    nums[i] = nums[i - 1];
                    nums[i - 1] = temp;
                }
            } else if (i != 0 && i % 2 == 0) {
                if (nums[i] > nums[i - 1]) {
                    int temp = nums[i];
                    nums[i] = nums[i - 1];
                    nums[i - 1] = temp;
                }
            }
        }
    }

29.(681:Medium)(经典题)Next Closest Time

  • Since there are at max 4 * 4 * 4 * 4 = 256 possible times, just try them all using DFS..
    int diff = Integer.MAX_VALUE;
    String result = "";
    
    public String nextClosestTime(String time) {
        Set<Integer> set = new HashSet<>();  //存储输入time中的所有digit
        set.add(Integer.parseInt(time.substring(0, 1)));
        set.add(Integer.parseInt(time.substring(1, 2)));
        set.add(Integer.parseInt(time.substring(3, 4)));
        set.add(Integer.parseInt(time.substring(4, 5)));
        
        if (set.size() == 1) return time;
        
        List<Integer> digits = new ArrayList<>(set);
        int minute = Integer.parseInt(time.substring(0, 2)) * 60 + Integer.parseInt(time.substring(3, 5)); //将time转换为分钟

        dfs(digits, "", 0, minute);

        return result;
    }

    private void dfs(List<Integer> digits, String cur, int pos, int target) { //cur是搜索路径,pos是当前位置,target是主函数输入的时间
        if (pos == 4) { //计算当前的搜索路径所代表的时间,转换为分钟制
            int m = Integer.parseInt(cur.substring(0, 2)) * 60 + Integer.parseInt(cur.substring(2, 4));
            if (m == target) return;
            int d = m - target > 0 ? m - target : 1440 + m - target; //如果m-target小于0,说明此时cur搜索路径代表的是第二天的时间
            if (d < diff) {
                diff = d;
                result = cur.substring(0, 2) + ":" + cur.substring(2, 4);
            }
            return;
        }

        for (int i = 0; i < digits.size(); i++) {
            if (pos == 0 && digits.get(i) > 2) continue;  //一天最多只有23个小时,十位数不可能大于2
            if (pos == 1 && Integer.parseInt(cur) * 10 + digits.get(i) > 23) continue; //一天最多只有23个小时,满24进1
            if (pos == 2 && digits.get(i) > 5) continue; //一小时最多只有59分钟,十位不能大于5
            if (pos == 3 && Integer.parseInt(cur.substring(2)) * 10 + digits.get(i) > 59) continue; //一小时只有59分钟,满60进1 
            dfs(digits, cur + digits.get(i), pos + 1, target);
        }
    }

29.(285:Medium)(经典题)Inorder Successor in BST

    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
        TreeNode succ = null;
        while (root != null) {
            if (p.val < root.val) {   //当p在root的左子树上时,才记录succ,因为当前的root可能就是p的successor
                succ = root;
                root = root.left;
            }
            else
                root = root.right;  //如果p在root的右子树上时,我们不记录succ,因为肯定存在比p还大的元素,或者是null
        }
        return succ;
    }

30.(548:Medium)(非常经典题)Split Array with Equal Sum

  • 该题和之前某道求三个子数组最大和的问题相似,我们首先确定中间指针的位置,然后再分别遍历左右两边的指针,需要用到累加和数组来O(1)时间求得子数组的和。参见下边的代码:

Here j is used for middle cut, i for left cut and k for right cut.

Iterate middle cuts and then find left cuts which divides the first half into two equal quarters, store that quarter sums in the hashset. Then find right cuts which divides the second half into two equal quarters and check if quarter sum is present in the hashset. If yes return true.

    public boolean splitArray(int[] nums) {
        if (nums.length < 7)
            return false;
        int[] sum = new int[nums.length]; //累加和数组
        sum[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            sum[i] = sum[i - 1] + nums[i];
        }
        for (int j = 3; j < nums.length - 3; j++) { //确定中间指针的活动范围
            HashSet<Integer> set = new HashSet<>(); 
            for (int i = 1; i < j - 1; i++) {
                if (sum[i - 1] == sum[j - 1] - sum[i])  //先确定前半部分的符合值,按定义来求值即可
                    set.add(sum[i - 1]);
            }
            for (int k = j + 2; k < nums.length - 1; k++) { //再遍历后半部分的符合值,且该值要与前半部分相同,按定义来求值即可
                if (sum[nums.length - 1] - sum[k] == sum[k - 1] - sum[j] && set.contains(sum[k - 1] - sum[j]))
                    return true;
            }
        }
        return false;
    }
  • 这种将数组多分割的问题,首先都需要先固定中间指针的位置,然后再移动首尾指针

31.(348:Medium)(非常经典题)Design Tic-Tac-Toe

  • Initially, I had not read the Hint in the question and came up with an O(n) solution. After reading the extremely helpful hint; a much easier approach became apparent. The key observation is that in order to win Tic-Tac-Toe you must have the entire row or column. Thus, we don't need to keep track of an entire n^2 board. We only need to keep a count for each row and column. If at any time a row or column matches the size of the board then that player has won.
  • To keep track of which player, I add one for Player1 and -1 for Player2. There are two additional variables to keep track of the count of the diagonals. Each time a player places a piece we just need to check the count of that row, column, diagonal and anti-diagonal.
  • 简单来说,我们假设player1的mark会在对应方向加1,player2的mark会在对应方向减1,那么只要某行、某列、对角线、反对角线上的绝对值之和与n相等,那么最后下棋的player就会获胜。我们注意到题目条件中,place是有效的,它只会放在空的block里,所以不用考虑两个player放置mark重叠的情况

32.(272:Hard)(非常经典题)Closest Binary Search Tree Value II

  • BST中序遍历的性质,数组元素从小到大排序,所以以inorder traversal遍历后的结果中,第一个元素一定是偏离target最远的元素。如果某一时刻,当前root的value值减去target的差异大于当前结果集中第一个元素的差异,之后就没必要遍历了,因为根据inorder遍历元素升序的关系,后面遍历到的结果会偏离target更远
  • 直接在中序遍历的过程中完成比较,当遍历到一个节点时,如果此时结果数组不到k个,我们直接将此节点值加入res中,如果该节点值和目标值的差值的绝对值小于res的首元素和目标值差值的绝对值,说明当前值更靠近目标值,则将首元素删除,末尾加上当前节点值,反之的话说明当前值比res中所有的值都更偏离目标值,由于中序遍历的特性,之后的值会更加的偏离,所以此时直接返回最终结果即可。

33.(562:Hard)(非常经典题)Longest Line of Consecutive One in Matrix

  • 该题的discuss解法中用了三维的DP,但第三维的个数是固定的4个,代表四个方向的最多连续1的个数,其实我们可以将其简化为二维DP,只不过需要四个二维DP数组来分别代表四个方向上连续1的个数。

34.(431:Hard)(非常非常经典题)Encode N-ary Tree to Binary Tree

  • 直接参看代码及注释
class Codec {
    // Encodes an n-ary tree to a binary tree.
    // Encode的时候,对于一个当前的根节点,转换后二叉树的左子树是原来N叉树的孩子结点,右子树是原来N叉树的兄弟结点
    public TreeNode encode(Node root) {
        if (root == null) {
            return null;
        }
        TreeNode result = new TreeNode(root.val);
        if (root.children.size() > 0) {
            result.left = encode(root.children.get(0));
        }
        TreeNode cur = result.left;
        for (int i = 1; i < root.children.size(); i++) {
            cur.right = encode(root.children.get(i));
            cur = cur.right;
        }
        return result;
    }

    // Decodes your binary tree to an n-ary tree.
    public Node decode(TreeNode root) {
        if (root == null) {
            return null;
        }
        Node result = new Node(root.val, new LinkedList<>());
        TreeNode cur = root.left;
        while (cur != null) {
            result.children.add(decode(cur)); //添加的是cur及其子树结点
            cur = cur.right;
        }
        return result;
    }
}

35.(163:Medium)(经典题)Missing Ranges

  • 该题主要是一些corner case比较多,且写出concise code有一定难度。下面的代码注意要将变量设为long类型,防止变量的溢出
public List<String> findMissingRanges(int[] A, int lower, int upper) {
    List<String> result = new ArrayList<String>();
    //注意变量的类型都是long,来handle一些corner cases
    long pre = (long)lower - 1;  
    for(int i = 0 ; i <= A.length  ; i++){
        long after = i == A.length ? (long)upper + 1 : (long)A[i]; 
        if(pre + 2 == after){ //检查是差一个数还是差一个序列
            result.add(String.valueOf(pre + 1));
        }else if(pre + 2 < after){
            result.add(String.valueOf(pre + 1) + "->" + String.valueOf(after - 1));
        }
        pre = after;
    }
    return result;
}

36.(776:Medium)(非常经典题)Split BST

  • 该题要用Backtracking进行求解,核心思想是:设立一个长度为2的一维数组,res[0]存储root with values <= V,res[1]存储root with values > V。可以直接参看下边的代码及注释,很详细:
public TreeNode[] splitBST(TreeNode root, int V) {
	// res[0]: root with values <= V
        // res[1]: root with values > V

        if (root == null)
        {
            return new TreeNode[]{null, null};
        }

        if (root.val <= V)
        {
            // The cut off is somewhere in the right subtree relative to root

            TreeNode[] res = splitBST(root.right, V);

            // res[0] is the node for all values <= V found in the right sub tree
            // since it's form the right sub tree, all values must be greater than root
            // so safe to do the following
            root.right = res[0];

            // root along with everything in its left subtree are already <= V
            // so after setting root.right=res[0], now root represents all nodes <= V

            // the node for all values greater than V is still res[1], some node found on the right subtree
            
            return new TreeNode[]{root, res[1]};
        }
        else //root.val > V
        {
            // The cut off is somewhere in the left subtree relative to root
                           
            TreeNode[] res = splitBST(root.left, V);

            // res[1] is the node for all values >V found in the left sub tree
            // since it's from the left sub tree, all values must be less than root
            // so safe to do the following
            
            root.left = res[1];
            
            // root along with everything in its right subtree are already >V
            // so after setting root.left=res[1], now root represents all values >V
            
            // the node for all values less than V is still res[0], some node found on the left subtree 
            
            return new TreeNode[]{res[0], root};
        }
}

37.(549:Medium)(非常经典题)Binary Tree Longest Consecutive Sequence II

  • 使用Backtracking,思路是要以每一个当前结点作为基准,来比较左右子树的最长连续递增子序列的长度,以后序遍历进行traverse
int max = 0;
class Result {  //用于统计以当前结点作为参考,记录左右子树的连续递增、连续递减序列长度
    TreeNode node;
    int inc;
    int des;
}
    
public int longestConsecutive(TreeNode root) {
    traverse(root);
    return max;
}
    
private Result traverse(TreeNode node) {
    //使用后序遍历的方法
    if (node == null) return null;
    
    Result left = traverse(node.left);
    Result right = traverse(node.right);
        
    Result curr = new Result();
    curr.node = node;
    curr.inc = 1;
    curr.des = 1;
        
    if (left != null) {
        if (node.val - left.node.val == 1) {
            curr.inc = Math.max(curr.inc, left.inc + 1);
        }
        else if (node.val - left.node.val == -1) {
            curr.des = Math.max(curr.des, left.des + 1);
        }
    }
        
    if (right != null) {
        if (node.val - right.node.val == 1) {
            curr.inc = Math.max(curr.inc, right.inc + 1);
        }
        else if (node.val - right.node.val == -1) {
            curr.des = Math.max(curr.des, right.des + 1);
        }
    }
        
    max = Math.max(max, curr.inc + curr.des - 1);  //每一层比较完后都会更新max返回值
        
    return curr;
}

38.(370:Medium)(非常经典题)Range Addition

  • 在这题中没必要每次把区间的每一个值都更新, 只需要更新起点和终点后的一点即可. 也就是说把增加的值放在起点, 终点后的一点减去增加的值, 这样再扫描的时候把之前累加的和作为最终值即可,例如从index 2到index4的值都要加2,那么index2=2,index3=0,index4=0,index5=-2,这样从左往右在做累加和的时候,从index2到index4均+2,到index5时加2-2=0,保证了增加的范围只有2至4。

39.(702:Medium)(非常经典题)Search in a Sorted Array of Unknown Size

  • To use binary search, we need to find the search space defined by low and hi. Find hi by moving hi exponentially. Once hi is found, low is previous hi. Then do binary search.

40.(320:Medium)(非常经典题)Generalized Abbreviation

  • The idea is: for every character, we can keep it or abbreviate it. To keep it, we add it to the current solution(但同时也要把现在已有的count添加到该字符的前边) and carry on backtracking. To abbreviate it, we omit it in the current solution, but increment the count, which indicates how many characters have we abbreviated. When we reach the end or need to put a character in the current solution, and count is bigger than zero, we add the number into the solution. 具体可以参看代码及注释。

41.(333:Medium)(非常经典题)Largest BST Subtree

  • 思路:先验证以当前结点为根的子树是否是BST,若是则统计树的大小;否则递归判断左右子树是否是BST。代码及注释如下:
    public int largestBSTSubtree(TreeNode root) {
        if (root == null) return 0;
        if (root.left == null && root.right == null) return 1;
        if (isValid(root, null, null)) return countNode(root); //验证以当前root为根节点的子树是否是BST,是的话则统计结点数
        return Math.max(largestBSTSubtree(root.left), largestBSTSubtree(root.right));
    }

    public boolean isValid(TreeNode root, Integer min, Integer max) { //验证以当前结点为根的树是否为BST
        if (root == null) return true;
        if (min != null && min >= root.val) return false;
        if (max != null && max <= root.val) return false;
        return isValid(root.left, min, root.val) && isValid(root.right, root.val, max);
    }

    public int countNode(TreeNode root) { //统计以当前结点为根的树的大小
        if (root == null) return 0;
        if (root.left == null && root.right == null) return 1;
        return 1 + countNode(root.left) + countNode(root.right);
    }

42.(298:Medium)(非常经典题)Binary Tree Longest Consecutive Sequence

  • 该题采用后序遍历进行递归求解,自底向上进行处理,取左右子树路径中较长的一条进行返回,在每一层都需要跟新res结果变量。具体可以参看代码实现

43.(256:Easy)(非常非常经典题)Paint House

  • Dynamic Programming. 在前一个房子已经涂好颜色的基础上计算当前房子,代码如下:
public int minCost(int[][] costs) {
    if(costs==null||costs.length==0){
        return 0;
    }
    for(int i=1; i<costs.length; i++){  //DP
        costs[i][0] += Math.min(costs[i-1][1],costs[i-1][2]);
        costs[i][1] += Math.min(costs[i-1][0],costs[i-1][2]);
        costs[i][2] += Math.min(costs[i-1][1],costs[i-1][0]);
    }
    int n = costs.length-1;
    return Math.min(Math.min(costs[n][0], costs[n][1]), costs[n][2]);
}

44.(265:Hard)(非常非常经典题)Paint House II

  • 这道题是之前那道Paint House的拓展,那道题只让用红绿蓝三种颜色来粉刷房子,而这道题让我们用k种颜色,这道题不能用之前那题的解法,会TLE。这题的解法的思路还是用DP,但是在找不同颜色的最小值不是遍历所有不同颜色,而是用min1和min2来记录之前房子的最小和第二小的花费的颜色,如果当前房子颜色和min1相同,那么我们用min2对应的值计算,反之我们用min1对应的值这种解法实际上也包含了求次小值的方法,感觉也是一种很棒的解题思路

45.(255:Medium)(非常经典题)Verify Preorder Sequence in Binary Search Tree

  • 二叉搜索树的性质是左<根<右,如果用中序遍历得到的结果就是有序数组,而先序遍历的结果就不是有序数组了,但是难道一点规律都没有了吗,其实规律还是有的,根据二叉搜索树的性质,当前节点的值一定大于其左子树中任何一个节点值,而且其右子树中的任何一个节点值都不能小于当前节点值,那么我们可以用这个性质来验证。比如下面这棵二叉搜索树:
     5
    / \
   2   6
  / \
 1   3
  • 其先序遍历的结果是{5, 2, 1, 3, 6}, 我们先设一个最小值low,然后遍历数组,如果当前值小于这个最小值low,返回false,对于根节点,我们将其压入栈中,然后往后遍历,如果遇到的数字比栈顶元素小,说明是其左子树的点,继续压入栈中,直到遇到的数字比栈顶元素大,那么就是右边的值了,我们需要找到是哪个节点的右子树,所以我们更新low值并删掉栈顶元素,然后继续和下一个栈顶元素比较,如果还是大于,则继续更新low值和删掉栈顶,直到栈为空或者当前栈顶元素大于当前值停止,压入当前值,这样如果遍历完整个数组之前都没有返回false的话,最后返回true即可。具体可参考代码。

46.(243:Easy)Shortest Word Distance

  • 用两个变量分别记录最新的word1和word2的位置,从左往右遍历数组并更新位置变量,每有一次更新时,更新结果值res。

47.(323:Medium)(经典题)Number of Connected Components in an Undirected Graph

  • 典型的Union Find题目
  • Union Find中路径压缩的代码如下:
public int find(int[] roots, int id) {
    while(roots[id] != id) {
        roots[id] = roots[roots[id]];  // optional: path compression
        id = roots[id];
    }
    return id;
}

48.(545:Medium)(非常经典题)Boundary of Binary Tree

  • 该题因为是顺时针顺序遍历边界元素,所以不能用单一的函数处理所有的情况,因此设立三个函数,分别遍历处理左边界、叶子结点和右边界。在运行左边界函数和右边界函数时,注意避开叶子结点,叶子节点的处理应单独再叶子结点函数中去访问
    List<Integer> list = new ArrayList<Integer>();
    public List<Integer> boundaryOfBinaryTree(TreeNode root) {
        if(root == null)
            return list;
        // Add the root to the result list
            list.add(root.val);
        // perform left boundary traversal
            leftBoundary(root.left);
        // Collect the leaf nodes of the left subtree
            collectLeafNodes(root.left);
        // Collect the leaf nodes of the right subtree
            collectLeafNodes(root.right);
        // perform right boundary traversal
            rightBoundary(root.right);
        
        return list;
    }
    
    public void leftBoundary(TreeNode root) {
        if(root != null){
            // print all the left nodes first ,if left is not there print the right nodes, if its a leaf node skip it
            if(root.left != null){
                // first add the result and then recurse call to maintain the top-down order
                list.add(root.val);
                leftBoundary(root.left);
            }else if(root.right != null){
                // first add the result and then recurse call to maintain the top-down order
                list.add(root.val);
                leftBoundary(root.right);           
            }
        }
    }
    
    public void rightBoundary(TreeNode root) {
        if(root != null){
            // print all the right nodes first ,if right is not there print the left nodes, if its a leaf node skip it
            if(root.right != null){
                // first recurse call then add to result to maintain the bottom-up order
                rightBoundary(root.right);
                list.add(root.val);
            }else if(root.left != null){
                   // first recurse call then add to result to maintain the bottom-up order
                rightBoundary(root.left); 
                list.add(root.val);          
            }
        }
    }
    
    public void collectLeafNodes(TreeNode root){
        if(root != null){
            // go to the left most depth
            collectLeafNodes(root.left);
            if(root.left == null && root.right == null)
                list.add(root.val);
            
            // go to the right half, and repeat going to its leftmost end
            collectLeafNodes(root.right);
        }
    }

49.(489:Medium)(非常非常经典题)Robot Room Cleaner

  • 该题使用DFS遍历所有能到达的位置。
  1. To track the cells the robot has cleaned, start a index pair (i, j) from (0, 0). When go up, i-1; go down, i+1; go left, j-1; go right: j+1.
  2. Also use DIR to record the current direction of the robot. 0: up, 90: right, 180: down, 270: left
    public void cleanRoom(Robot robot) {
        // A number can be added to each visited cell
        // use string to identify the class
        Set<String> set = new HashSet<>();
        int cur_dir = 0;   // 0: up, 90: right, 180: down, 270: left
        backtrack(robot, set, 0, 0, 0);
    }

    public void backtrack(Robot robot, Set<String> set, int i, int j, int cur_dir) {
	String tmp = i + "->" + j;
	if(set.contains(tmp)) 
	     return;
	        
	robot.clean();
	set.add(tmp);
	
	 for(int n = 0; n < 4; n++) {
	        // the robot can to four directions, we use right turn
	    	if(robot.move()) {
	    		// can go directly. Find the (x, y) for the next cell based on current direction
	    		int x = i, y = j;
	    		switch(cur_dir) {  //还是按照当前既定的方向前进
	    			case 0:
	    				// go up, reduce i
	    				x = i-1;
	    				break;
	    			case 90:
	    				// go right
	    				y = j+1;
	    				break;
	    			case 180:
	    				// go down
	    				x = i+1;
	    				break;
	    			case 270:
	    				// go left
	    				y = j-1;
	    				break;
	    			default:
	    				break;
	    		}
	
	    	        backtrack(robot, set, x, y, cur_dir);  //更新坐标后,方向并未改变
	                // go back to the starting position,因为要遍历上下左右所有方向
			robot.turnLeft();
	    		robot.turnLeft();
	    		robot.move();
	    		robot.turnRight();
	    		robot.turnRight();
	    	} 
	    	// turn to next direction
	    	robot.turnRight();
	    	cur_dir += 90;
	    	cur_dir %= 360;
	    }
    }

50.(305:Hard)(非常非常经典题)Number of Islands II

  • 该题使用Union-Find来进行求解。我们需要将一个position (x,y)转化为一个id便于find查找根,这里使用自定义一个规则int id = n * x + y; n是行数
  • Initially assume every cell are in non-island set {-1}. When point A is added, we create a new root, i.e., a new island. Then, check if any of its 4 neighbors belong to the same island. If not, union the neighbor by setting the root to be the same. Remember to skip non-island cells.

UNION operation is only changing the root parent so the running time is O(1).

FIND operation is proportional to the depth of the tree. If N is the number of points added, the average running time is O(logN), and a sequence of 4N operations take O(NlogN). If there is no balancing, the worse case could be O(N^2).

  • 我们可以使用路径压缩来缩短查找根的时间

51.(159:Hard)(非常非常经典题)Longest Substring with At Most Two Distinct Characters

  • 该题使用滑动窗口来解决,用一个map来记录窗口中的字符及每个字符最后一次出现的位置(这样便于将最开始出现的字符删去),在窗口右边界向右滑动的过程中,始终保证窗口内字符的种类为2。参考代码如下:
    public int lengthOfLongestSubstringTwoDistinct(String s) {
        if(s.length() < 1) return 0;
        HashMap<Character,Integer> index = new HashMap<Character,Integer>(); //key是当前窗口内的字符,value是该字符最后一次出现的位置
        int lo = 0;
        int hi = 0;
        int maxLength = 0;  //结果变量
        while(hi < s.length()) {
        		//注意下边的两个if语句是并列的,假如新加入的字符是之前出现过的,map的长度并不改变,lo不变,hi+1
            if(index.size() <= 2) {
                char c = s.charAt(hi);
                index.put(c, hi);
                hi++;
            }
            if(index.size() > 2) {  //删去最早出现的字符,并更新lo值
                int leftMost = s.length();
                for(int i : index.values()) {
                    leftMost = Math.min(leftMost,i);
                }
                char c = s.charAt(leftMost);
                index.remove(c);
                lo = leftMost+1;
            }
            maxLength = Math.max(maxLength, hi-lo);
        }
        return maxLength;
    }

52.(186:Medium)(非常经典题)Reverse Words in a String II

  • 正确的解法参考如下,具体思路解释请参看注释
    public void reverseWords(char[] s) {
        // Three step to reverse
        // 1, reverse the whole sentence,保证每个单词在reverse前占用的空间是正确的,即eulb对应blue的位置
        reverse(s, 0, s.length - 1);
        // 2, reverse each word
        int start = 0;
        int end = -1;
        for (int i = 0; i < s.length; i++) {
            if (s[i] == ' ') {
                reverse(s, start, i - 1);
                start = i + 1;
            }
        }
        // 3, reverse the last word, if there is only one word this will solve the corner case
        reverse(s, start, s.length - 1);
    }

    public void reverse(char[] s, int start, int end) {
        while (start < end) {
            char temp = s[start];
            s[start] = s[end];
            s[end] = temp;
            start++;
            end--;
        }
    }

53.(487:Medium)(非常非常经典题)Max Consecutive Ones II

  • 我们需要用一个变量curr来记录当前连续1的个数,那么当遇到了0的时候,因为我们有一次0变1的机会,所以我们遇到0了还是要累加curr。然后我们此时需要用另外一个变量pre来保存当前curr的值,然后curr重置为0,以便于让curr一直用来统计纯连续1的个数,然后我们每次都用用pre+curr来更新结果res。具体意会的代码如下:
public int findMaxConsecutiveOnes(int[] nums) {
    if(nums == null || nums.length == 0)
        return 0;
    int res = 0, preCnt = 0, curCnt = 0;
    for (int i = 0; i < nums.length; i++) {
        //这里不判断当前i指向的数字是否为1,均将curCnt加一,是因为题目允许翻转一个0,当当前的数字是0时,由下面的if语句handle
        curCnt++; //该思路是把所有翻转的1都归咎到前半段上
        if (nums[i] == 0) {
            preCnt = curCnt; //如果nums=[1,1,1,0,1,1],当i=3时,preCnt是4,curCnt是0; 如果nums=[1,1,1,0,0,1], i=4,那么此时preCnt=1, curCnt=0
            curCnt = 0;
        }
        res = Math.max(res, preCnt + curCnt);
    }
    return res;
}

54.(249:Medium)(经典题)Group Shifted Strings

  • 该道题的核心是:例如'cbd','efg','xyz'都能用'abc'类型来表示,因为它们长度相同,且与'abc'的偏移一致。所以我们需要找出每个string字符串序列的类型,并将它们归类在一起
public List<List<String>> groupStrings(String[] strings) {
    List<List<String>> result = new ArrayList<List<String>>();
    Map<String, List<String>> map = new HashMap<String, List<String>>(); //map的key就是上面的类型,如'abc'
    for (String str : strings) {
        int offset = str.charAt(0) - 'a'; //计算偏移量
        String key = "";
        //下面计算当前字符串str的类型
        for (int i = 0; i < str.length(); i++) {
            char c = (char) (str.charAt(i) - offset);
            if (c < 'a') {
                c += 26;
            }
            key += c;
        }
        if (!map.containsKey(key)) { //看看之前是否有该类型出现过
            List<String> list = new ArrayList<String>();
            map.put(key, list);
        }
        map.get(key).add(str);
    }
    for (String key : map.keySet()) { //整理成符合题目要求格式的结果集
        List<String> list = map.get(key);
        Collections.sort(list); //对每个类型的list进行字母序排序
        result.add(list);
    }
    return result;
}

55.(536:Medium)(非常非常经典题)Construct Binary Tree from String

  • 该题使用recursive来构建树,在每一层递归过程中,先处理根节点,然后利用括号匹配原则,依次扫描出左子树的字符串序列和右子树的字符串序列,然后对左右子树的字符串分别进行递归调用建树。参考代码如下:
public TreeNode str2tree(String s) {
    // Base case
    if (s.length() == 0) return null; //空结点

    // Create root
    int i = 0, j = 0;
    while (j < s.length() && (Character.isDigit(s.charAt(j)) || s.charAt(j) == '-')) j++;  //可能树节点的值为1234,多位数字
    TreeNode root = new TreeNode(Integer.parseInt(s.substring(i, j)));

    //到该位置,j指向的应该是最外层左括号的索引

    // Left child
    if (j < s.length()) {
        i = j; //当前i指向的是下一层左子树左括号的位置
        int count = 1; //count计算括号未匹配的数量,1代表初始时为左括号
        while (j + 1 < s.length() && count != 0) { //count=0代表所有括号均已匹配,下一层的遍历结束,进行深层次递归处理
            j++; //此时指向下一层的第一个数字
            if (s.charAt(j) == ')') count--; //右括号count--
            if (s.charAt(j) == '(') count++; //左括号count++
        }
        root.left = str2tree(s.substring(i + 1, j)); // i+1才是下一层数字真正开始的地方,j现在指向下一层左子树的右括号位置
    }

    j++; //j现在指向下一层右子树的左括号位置
    // Right child
    if (j < s.length()) {
        root.right = str2tree(s.substring(j + 1, s.length() - 1));
    }

    return root;
}

56.(366:Medium)(非常经典题)Find Leaves of Binary Tree

  • For this question we need to take bottom-up approach. The key is to find the height of each node. Here the definition of height is: The height of a node is the number of edges from the node to the deepest leaf.
  • I used a helper function to return the height of current node. According to the definition, the height of leaf is 0. h(node) = 1 + max(h(node.left), h(node.right)).

57.(364:Medium)(非常经典题)Nested List Weight Sum II

  • 该题的思路是:在计算某一层的数字和时,都要考虑该层以上的各数字之和,从而加入了权重的概念,如下例所示
  • 例如 list=[3,[2,[1]]]sum = 3 + (3 + 2) + (3 + 2 + 1),分别代表第三层,第二层,第一层的数字和。参考代码如下:
public int depthSumInverse(List<NestedInteger> nestedList) {
    int prevSum = 0, totalSum = 0; // preSum代表当前层以上各层的数字之和,totalSum代表到目前层为止的所有数字和
    Queue<NestedInteger> queue = new LinkedList<>();
    for (NestedInteger ni : nestedList) {
        queue.add(ni);
    }

    while (!queue.isEmpty()) {
        int size = queue.size(), levelSum = 0; //levelSum代表当前层的和,不考虑当前层以上各层数字的和
        for (int i = 0; i < size; i++) {
            NestedInteger current = queue.poll();
            if (current.isInteger()) {
                levelSum += current.getInteger();
            } else {
                for (NestedInteger ni: current.getList()) {
                    queue.add(ni);
                }
            }
        }
        prevSum += levelSum; // 将当前层以上各层数字的和也考虑进去
        totalSum += prevSum;
    }
    return totalSum;
}

58.(362:Medium)(经典题)Design Hit Counter

  • Use a queue to record the information of all the hits. Each time we call the function getHits( ), we have to delete the elements which hits beyond 5 mins (300). The result would be the length of the queue.

59.(156:Medium)(经典题)Binary Tree Upside Down

  • 对于子树A,其上的各节点的父子兄弟关系顺时针移动一个单位就是变换后的子树.对于一个有着左右孩子的子树,它的左子树根节点成为新的root,root变成新子树的右结点,原来的右子树根节点变成新子树的左结点。参考代码如下:
public TreeNode upsideDownBinaryTree(TreeNode root) {
    if (root == null || root.left == null && root.right == null)
        return root;

    TreeNode newRoot = upsideDownBinaryTree(root.left); //找新的根节点

    //构建新的子树关系
    root.left.left = root.right;
    root.left.right = root;

    //将新子树的原来root结点左右子树置空
    root.left = null;
    root.right = null;

    return newRoot;
}

60.(244:Medium)(非常经典题)Shortest Word Distance II

  • 我们用一个map存储数组中的每一个单词及其出现的所有位置。然后维护两个指向输入的两个字符串对应的位置list指针,move forward比较找出最小的差值。具体思路可参考代码实现。

61.(254:Medium)(非常经典题)Factor Combinations

  • 该题就是使用典型的recursive DFS的方法,将因子从小到大进行尝试,看看是否能被当前的数字整除,若能整除则加入到当前的path list里,注意,因子的尝试要保证从小到大,这样才能保证答案的结果唯一,不重复。参考代码如下:
public List<List<Integer>> getFactors(int n) {
    List<List<Integer>> result = new ArrayList<List<Integer>>(); //结果集
    helper(result, new ArrayList<Integer>(), n, 2); //DFS函数, start从2开始,不考虑因子1及其本身
    return result;
}

public void helper(List<List<Integer>> result, List<Integer> item, int n, int start){
    if (n <= 1) {
        if (item.size() > 1) { // 判断是否为一个有效的factor combination
            result.add(new ArrayList<Integer>(item));
        }
        return;
    }

    // 尝试所有可能的factor,注意因子是从start开始尝试,这样可以保证因子是升序记录的,最终的结果不会有重复
    for (int i = start; i <= n; ++i) {
        if (n % i == 0) {
            item.add(i); //加上i
            helper(result, item, n/i, i); //注意下一层循环的起始因子值是i,而不是当前层的start,去重复
            item.remove(item.size()-1); //去掉i
        }
    }
}

62.(296:Hard)(非常非常经典题)Best Meeting Point

  • 我们可以发现,由distance(p1, p2) = |p2.x - p1.x| + |p2.y - p1.y|曼哈顿距离的x坐标和y坐标的计算是毫无关系的,所以,我们可以将二维坐标转化为一维坐标,分别计算两个方向的最短相遇距离,从而得到答案
  • 基于以上分析,我们分别计算x坐标和y坐标的最短相遇距离。偶数情况:假如有两个点1-0-0-0-0-1,不论最短的相遇位置在哪里,两个点都需要移动5个单位的距离。奇数情况:假设有三个点1-0-0-0-1-0-1,最短的相遇位置很明显就是中间的1的位置,所以最短的移动距离仍然是5。基于此,我们对于一个方向的坐标,维护两个指针i和j分别从头尾位置向中间遍历,计算距离的差值和即可。具体思路可以参考代码实现。

63.(544:Medium)(经典题)Output Contest Matches

  • 注意:每次都是折半的和倒数的相对位置字符串进行拼接。代码如下:
public String findContestMatch(int n) {
    String[] m = new String[n];
    for (int i = 0; i < n; i++) {
        m[i] = String.valueOf(i + 1);
    }

    while (n > 1) {
        for (int i = 0; i < n / 2; i++) {
            m[i] = "(" + m[i] + "," + m[n - 1 - i] + ")"; // 注意,每次都是折半的和倒数的相对位置字符串进行拼接
        }
        n /= 2; //n也是折半二分
    }

    return m[0];
}

64.(1055:Medium)(非常经典题)Shortest Way to Form String

  • We traverse the target string while matching source string multiple times.
t: index for target string
s: index for source string
ans: results
  • Each time, we want to ensure that t index moves forward instead of staying steady. 我们使t一次移动尽量远的距离,代码如下:
public int shortestWay(String source, String target) {
    int t = 0; //指向target字符串的指针,只会一直前进
    int ans = 0; //结果,需要多少个source的subsequence来构成target
    while (t < target.length()) {
        int pt = t; //记录一下当前loop初始时刻t的位置

        for (int s = 0; s < source.length(); s++) {
            //因为我们是拿source的subsequence去与target进行匹配,所以尽量的最大匹配,只要保证匹配的顺序一致就可以
            //假如source字符串是ababc,匹配的是abxxc还是xxabc效果均相同
            if (t < target.length() && source.charAt(s) == target.charAt(t)) {
                t++;
            }
        }

        //假如比较完source字符串后,t没有移动,说明target中一定存在source中没有的字符
        if (t == pt) {
            return -1;
        }
        ans++;
    }

    return ans;
}

65.(484:Medium)(非常非常经典题)Find Permutation

  • 这道题给了我们一个由D和I两个字符组成的字符串,分别表示对应位置的升序和降序,要我们根据这个字符串生成对应的数字字符串。由于受名字中的permutation的影响,感觉做法应该是找出所有的全排列然后逐个数字验证,这种方法十有八九无法通过OJ。其实这题用贪婪算法最为简单,我们来看一个例子:

D D I I D I

1 2 3 4 5 6 7

3 2 1 4 6 5 7

  • 我们不难看出,只有D对应的位置附近的数字才需要变换,而且变换方法就是倒置一下字符串,我们要做的就是通过D的位置来确定需要倒置的子字符串的起始位置和长度即可。通过观察,我们需要记录D的起始位置i,还有D的连续个数k,那么我们只需要在数组中倒置[i, i+k]之间的数字即可

66.(369:Medium)(非常经典题)Plus One Linked List

  • 很明显,这道题最好的方法是利用recursive,用递归的方法到达链表的最后一个结点,然后将链表最后一个节点的值加1,如果有进位carrybit为1,那么向上层返回,然后依次返回到头结点,如果头结点也有进位,说明应该返回一个新的头结点1,相当于999+1=1000.

67.(510:Medium)(非常经典题)Inorder Successor in BST II

  • 这道题是之前的那道 Inorder Successor in BST 的后续,之前那道题给了我们树的根结点,而这道题并没有确定给我们根结点,只是给了树的任意一个结点,然后让求给定结点的中序后继结点。这道题比较好的一点就是例子给的比较详尽,基本覆盖到了大部分的情况,包括一些很 tricky 的情况。首先来看例子1,结点1的中序后继结点是2,因为中序遍历的顺序是左-根-右。还是例子1,结点2的中序后续结点是3,这样我们就知道中序后续结点可以是其父结点或者右子结点。再看例子2,结点6的中序后续结点是空,因为其已经是中序遍历的最后一个结点了,所以没有后续结点。例子3比较 tricky,结点 15 的中序后续结点不是其右子结点,而是其右子结点的左子结点 17,这样才符合左-根-右的顺序。例子4同样 tricky,结点 13 的中序后继结点并不是其亲生父结点,而是其祖爷爷结点 15
  • 看完了这四个例子,我们知道后继结点出现的位置大致可以分为两类一类是在子孙结点中另一类是在祖先结点中。仔细观察例子不难发现,当某个结点存在右子结点时,其中序后继结点就在子孙结点中,反之则在祖先结点中。这样我们就可以分别来处理,当右子结点存在时,我们需要找到右子结点的最左子结点,这个不难,就用个 while 循环就行了。当右子结点不存在,我们就要找到第一个比其值大的祖先结点,也是用个 while 循环去找即可
  • 本题的 Follow up 让我们不要访问结点值,那么上面的解法就不行了。因为当 node 没有右子结点时,我们没法通过比较结点值来找到第一个大于 node 的祖先结点。虽然不能比较结点值了,我们还是可以通过 node 相对于其 parent 的位置来判断,当 node 是其 parent 的左子结点时,我们知道此时 parent 的结点值一定大于 node,因为这是二叉搜索树的性质。若 node 是其 parent 的右子结点时,则将 node 赋值为其 parent,继续向上找,直到其 parent 结点不存在了,此时说明不存在大于 node 值的祖先结点,这说明 node 是 BST 的最后一个结点了,没有后继结点,直接返回 null 即可

68.(250:Medium)(非常经典题)Count Univalue Subtrees

  • 该题是一个很经典的recursive的题目,直接看代码实现来理解该题本质
int count = 0;
public int countUnivalSubtrees(TreeNode root) {
    helper(root);
    return count;
}

private boolean helper(TreeNode node) {
    if (node == null) { //当子树为空时,默认返回true,不影响上面母树的比较
        return true;
    }
    boolean left = helper(node.left); //查看左子树是否unique
    boolean right = helper(node.right); //查看右子树是否unique
    if (left && right) { //如果左右子树都unique,则比较左右子树的值与根节点的值是否相等
        if (node.left != null && node.val != node.left.val) {
            return false;
        }
        if (node.right != null && node.val != node.right.val) {
            return false;
        }
        //若根节点、左右子树均相等,则将count+1
        count++;
        return true;
    }
    return false;
}

69.(1066:Medium)(非常非常经典题)Campus Bikes II

  • 该题用DFS解决非常容易,主要思想是:对当前的人,将每一辆未使用的bike assign给他,计算并选取最小的cost。代码思路如下:
    public int assignBikes(int[][] workers, int[][] bikes) {
        dfs(workers, 0, bikes, new boolean[bikes.length], 0);
        return min;
    }

    int min = Integer.MAX_VALUE;  //cost
    void dfs(int[][] workers, int i, int[][] bikes, boolean[] used, int sum) {
        if (i == workers.length) {  //检查是不是所有人都已经assign了bike
            min = Math.min(min, sum);
            return;
        }

        if (sum > min) return;  // early termination

        for (int j = 0; j < bikes.length; ++j) {  //尝试将所有未使用的bike都assign给当前这个人,选取最小的cost
            if (used[j]) continue;
            used[j] = true;
            dfs(workers, i+1, bikes, used, sum + getDistance(workers[i], bikes[j]));
            used[j] = false;
        }
    }

    int getDistance(int[] worker, int[] bike) {
        return Math.abs(worker[0] - bike[0]) + Math.abs(worker[1] - bike[1]);
    }

70.(379:Medium)(非常经典题)Design Phone Directory

  • 用set来存储已经使用过的电话号码即可,具体设计直接看下面代码:
Set<Integer> used = new HashSet<Integer>();
Queue<Integer> available = new LinkedList<Integer>();
int max;

/** Initialize your data structure here
 @param maxNumbers - The maximum numbers that can be stored in the phone directory. */
public _379_DesignPhoneDirectory(int maxNumbers) {
    max = maxNumbers;
    for (int i = 0; i < maxNumbers; i++) {
        available.offer(i);
    }
}

/** Provide a number which is not assigned to anyone.
 @return - Return an available number. Return -1 if none is available. */
public int get() {
    Integer ret = available.poll();
    if (ret == null) {
        return -1;
    }
    used.add(ret);
    return ret;
}

/** Check if a number is available or not. */
public boolean check(int number) {
    if (number >= max || number < 0) {
        return false;
    }
    return !used.contains(number);
}

/** Recycle or release a number. */
public void release(int number) {
    if (used.remove(number)) {
        available.offer(number);
    }
}

71.(742:Medium)(非常非常经典题)Closest Leaf in a Binary Tree

  • 核心思路将tree看成一个图,从target Node开始BFS寻找叶子结点难点在寻找target node时,需要用一个map记录孩子结点到parent结点的有向边,这样在从target node开始BFS时,相当于从三个方向进行搜索:左子树、右子树和parent结点
    public int findClosestLeaf(TreeNode root, int k) {
        Map<TreeNode, TreeNode> backMap = new HashMap<>();   // store all edges that trace node back to its parent
        Queue<TreeNode> queue = new LinkedList<>();          // the queue used in BFS
        Set<TreeNode> visited = new HashSet<>();             // store all visited nodes

        // DFS: search for node whoes val == k
        TreeNode kNode = DFS(root, k, backMap); //kNode使我们寻找的结点
        queue.add(kNode);
        visited.add(kNode);

        // BFS: find the shortest path
        while(!queue.isEmpty()) {
            TreeNode curr = queue.poll();
            if(curr.left == null && curr.right == null) {
                return curr.val;
            }
            if(curr.left != null && visited.add(curr.left)) {
                queue.add(curr.left);
            }
            if(curr.right != null && visited.add(curr.right)) {
                queue.add(curr.right);
            }
            //说白了就是parent结点的方向
            if(backMap.containsKey(curr) && visited.add(backMap.get(curr))) {  // go alone the back edge
                queue.add(backMap.get(curr));
            }
        }
        return -1; // never hit
    }

    private TreeNode DFS(TreeNode root, int k, Map<TreeNode, TreeNode> backMap) {
        if(root.val == k) {
            return root;
        }
        if(root.left != null) {
            // backMap存储反向遍历的边,为了在之后从target node BFS时,从三个方向left、right、backTrace(说白了就是parent结点)进行搜索
            backMap.put(root.left, root);        // add back edge
            TreeNode left = DFS(root.left, k, backMap);
            if(left != null) return left;
        }
        if(root.right != null) {
            backMap.put(root.right, root);       // add back edge
            TreeNode right = DFS(root.right, k, backMap);
            if(right != null) return right;
        }
        return null;
    }

72.(616:Medium)(非常非常经典题)Add Bold Tag in String

  • 对字符串的每一个位置i,调用String的API startsWith找到一次性可以加粗的最远位置,用长度为string.length()的boolean数组记录每个字符是否被加粗,只有当前的字符在一次性加粗的最远范围内时,它对应的boolean值才为true,最后根据boolean数组再遍历一次字符串返回最终的结果。
public String addBoldTag(String s, String[] dict) {
    boolean[] bold = new boolean[s.length()]; // bold[i]表示第i字符是否需要bold
    for (int i = 0, end = 0; i < s.length(); i++) {
        for (String word : dict) {
            if (s.startsWith(word, i)) { //利用String的API来判断从当前位置开始(inclusive),是否有dict中的word
                end = Math.max(end, i + word.length());//end表示以字符i开头,最长能bold的位置
            }
        }
        bold[i] = end > i; //当end > i时,当前的第i个字符才能加粗
    }

    StringBuilder result = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
        if (!bold[i]) { //不用加粗的字符直接append
            result.append(s.charAt(i));
            continue;
        }
        int j = i;
        while (j < s.length() && bold[j]) j++; //寻找当前tag能加粗的字符串结尾位置
        result.append("<b>" + s.substring(i, j) + "</b>");
        i = j - 1;
    }
    return result.toString();
}

73.(737:Medium)(非常非常经典题)Sentence Similarity II

  • This is a good use case for Union-Find, compare to Sentence Similarity I, here the similarity between words are transitive, so all the connected(similar) words should be group into an union represented by their ultimate parent(or family holder, you name it).
  • The connections can be represented by an parent map Map<String, String> m, which record the direct parent-ship we learned in each pair, but not the ultimate-parent. To build it, go through the input pairs, for each pair<w1, w2>, use the recursive find() method to find the ultimate-parent for both word - parent1, parent2, if they are different, assign parent1 as parent of parent2(or the other way around), so that the to families are merged.
  • The classic find(x) method will find the ultimate-parent of x. I modified it a little bit, make it do a little of extra initialization work - assign x itself as its parent when it is not initialize - so that we don't have to explicitly initialize the map at the beginning.

74.(505:Medium)(非常非常经典题)The Maze II

  • 该题解法和The Maze相似,不同的地方是用一个额外的二维数组去记录每一个可达maze位置的最短距离,而不是在循环遍历的时候一旦访问到了destination就返回那一时刻的距离。此外,使用pq优先去处理距离出发点距离较近的点。
    class Point {
        int x,y,l;
        public Point(int _x, int _y, int _l) {x=_x;y=_y;l=_l;}
    }
    public int shortestDistance(int[][] maze, int[] start, int[] destination) {
        int m=maze.length, n=maze[0].length;
        int[][] length=new int[m][n]; // record length,用来存储到达maze[i][j]的最短距离
        for (int i=0;i<m*n;i++) length[i/n][i%n]=Integer.MAX_VALUE;
        int[][] dir=new int[][] {{-1,0},{0,1},{1,0},{0,-1}};
        PriorityQueue<Point> list=new PriorityQueue<>((o1,o2)->o1.l-o2.l); // using priority queue,按距离远近进行排序
        list.offer(new Point(start[0], start[1], 0));
        while (!list.isEmpty()) {
            Point p=list.poll();
            if (length[p.x][p.y]<=p.l) continue; // if we have already found a route shorter
            length[p.x][p.y]=p.l;
            for (int i=0;i<4;i++) {
                int xx=p.x, yy=p.y, l=p.l;
                while (xx>=0 && xx<m && yy>=0 && yy<n && maze[xx][yy]==0) {
                    xx+=dir[i][0];
                    yy+=dir[i][1];
                    l++; //这里说明,我们重复走原来已经走过的路程,也是需要算重叠的距离的
                }
                xx-=dir[i][0];
                yy-=dir[i][1];
                //我们并不是当(xx,yy)就是destination的时候就返回距离,而是选取全局所有情况中最短的距离返回
                l--;
                list.offer(new Point(xx, yy, l));
            }
        }
        return length[destination[0]][destination[1]]==Integer.MAX_VALUE?-1:length[destination[0]][destination[1]];
    }

75.(774:Hard)(非常非常经典题)Minimize Max Distance to Gas Station

  • 这道题说给了我们n个加油站,两两之间相距不同的距离,然后我们可以在任意地方新加K个加油站,问能使得任意两个加油站之间的最大距离的最小值是多少。乍眼一看,感觉很绕,一会儿最大,一会儿最小的。其实我们可以换个场景,比如n个人站一队,每两个人之间距离不同,有的人之间距离可能很大,有的人可能挨得很近。我们现在需要再加入K个人到队列中,我们希望人与人之间的距离尽可能小,所以新人就应该加入到距离大的地方,然后问我们加入K个人后,求人与人之间的最大距离。这么一说,是不是清晰一点了呢。博主最开始看到这个加油站的题,以为跟之前那道Gas Station有关联,结果发现二者并没有什么关系,只不过公用了加油站这个场景而已。对于这道题,我们还是抽离出本质,就是数组插数问题。博主最先考虑的是用贪婪算法,就是先算出每两个数字之间的距离,然后我们每次往距离最大的那两个数字之间插入一个数字,这种想法看似正确,但是会跪在这样一个test case:
[10, 19, 25, 27, 56, 63, 70, 87, 96, 97],K = 3

其两两之间的距离为:

9,6,2,29,7,7,17,9,1
  • 如果按照博主前面所说的方法,会先将29分开,变成两个14.5,然后会将17分开,变成两个8.5,还剩一个加油站,会将其中一个14.5分开,变成两个7.25。但是这样弄下来,最大的距离还是14.5,而实际上我们有更好的办法,我们用两个加油站将29三等分,会变成三个9.67,然后用剩下的一个去分17,得到两个8.5,此时最大距离就变成了9.67,这才是最优的解法。
  • 其实正确的方法是使用二分查找寻找最短的可行的间距。Now we are using binary search to find the smallest possible value of D. I initilze left = 0 and right = the distance between the first and the last station(最远距离)count is the number of gas station we need to make it possible.count是需要完成最短距离mid所需要的额外station数量。
  1. if count > K, it means mid 选取的太小了,无法用额外的K个station来实现,应该扩大mid
  2. if count <= K, it means mid is possible,额外的K个station够用 and 我们可以尝试寻找更小的mid.
  • When left + 1e-6 >= right, it means the answer within 10^-6 of the true value and it will be accepted.

76.(660:Hard)(非常非常经典题)Remove 9

  • 我们会注意到,十进制数每一位均由0到9表示,二进制数每一位均由0到1表示,因为题目中不让出现9,所以我们应该使用九进制数用0到8表示,所以该题实际上考察的是将十进制数转化为九进制数。
  • 十进制转九进制的思路和十进制转二进制的思路相同,请参看该图十进制转二进制
  • 我们的解法模仿相同的逻辑即可
public int newInteger(int n) {
    int ans = 0;
    int base = 1;
    while (n > 0){
	ans += n % 9 * base;
	n /= 9;
	base *= 10;
    }
    return ans;
}

77.(527:Hard)(非常非常经典题)Word Abbreviation

  • 这道题让我们求单词的缩写形式,就是首尾字母加上中间字符的个数组成的新字符串,但是要求是不能有重复的缩写字符串,而且说明如果缩写字符串的长度并没有减小的话就保留原来的字符串,比如god,缩写成g1d也没啥用,所以仍是god。博主刚开始在研究题目中给的例子的时候有些疑惑,虽然知道internal和interval的缩写形式都是i6l,会冲突,博主刚开始不明白的是,为什么不能一个是i6l,一个是in5l,这样不就不冲突了么,而题目中的缩写形式居然都是原字符串。后来才搞清楚题目原来是说只要有冲突的都不能用,而internal和interval是典型的死杠上的一对,i6l,in5l,int4l,inte3l,inter2l,统统冲突,而再往后的缩写长度就和原字符串一样了,所以二者就都保留了原样。
  • 理解了题意就好办了,由于每个单词的缩写形式中数字前面的字母个数不一定相同,所以我们用一个pre数组来记录每个单词缩写形式开头字母的长度,初始化都为1,然后先求出所有单词pre为1的缩写形式,再来进行冲突处理。我们遍历每一个缩写字符串,进行while循环,新建一个集合set,然后遍历其他所有字符串,所有发现冲突字符串,就把冲突字符串的坐标存入集合中,如果没有冲突,那么集合为空,直接break掉,如果由冲突,那么还要把当前遍历的位置i加入结合中,然后遍历集合中所有的位置,对其调用缩写函数,此时pre对应的值自增1,直到没有冲突存在为止。

78.(248:Hard)(非常经典题)Strobogrammatic Number III

  • reuse Strobogrammatic Number II的方法。在获得全部结果以后,挑选出来值在上下界之间的数。
    public int strobogrammaticInRange(String low, String high){
        int count = 0;
        List<String> rst = new ArrayList<String>();
        for(int n = low.length(); n <= high.length(); n++){
            rst.addAll(helper(n, n));
        }
        for(String num : rst){

            if((num.length() == low.length()&&num.compareTo(low) < 0 ) ||(num.length() == high.length() && num.compareTo(high) > 0)) continue;
            count++;
        }
        return count;
    }

    private List<String> helper(int cur, int max){
        if(cur == 0) return new ArrayList<String>(Arrays.asList(""));
        if(cur == 1) return new ArrayList<String>(Arrays.asList("1", "8", "0"));

        List<String> rst = new ArrayList<String>();
        List<String> center = helper(cur - 2, max);

        for(int i = 0; i < center.size(); i++){
            String tmp = center.get(i);
            if(cur != max) rst.add("0" + tmp + "0");
            rst.add("1" + tmp + "1");
            rst.add("6" + tmp + "9");
            rst.add("8" + tmp + "8");
            rst.add("9" + tmp + "6");
        }
        return rst;
    }

79.(465:Hard)(非常非常经典题)Optimal Account Balancing

  • 该题有两种方法,一种是DFS,另一种是shuffle1000次求最小值。个人认为DFS的解法更好,面试时更具有说服力。下面将两种解法都贴在下边,根据注释即可理解对应的思路。
//DFS
private int count = 0;
public int minTransfers(int[][] transactions) {
    Map<Integer, Integer> balance = new HashMap<>();

    for(int[] t : transactions) { //计算每个人的净利润
        balance.put(t[0], balance.getOrDefault(t[0], 0) - t[2]);
        balance.put(t[1], balance.getOrDefault(t[1], 0) + t[2]);
    }

    int[] nums = new int[balance.size()]; //将每个人的净利润存到一个数组里
    int i = 0;
    for(int val : balance.values()) {
        nums[i++] = val;
    }

    return dfs(nums, 0); //从第一个人开始遍历
}

public int dfs(int[] nums, int i) {
    while(i < nums.length && nums[i] == 0) { //对于净利润为0的人我们不需要交易直接跳过
        ++i;
    }

    if(i == nums.length) { //如果index遍历到末尾,返回0,不需要再进行额外的交易
        return 0;
    }

    int count = Integer.MAX_VALUE; //将[i,n]这个范围内的人还清债务需要最少的交易次数
    for(int j = i + 1; j < nums.length; ++j) {
        if(nums[i] * nums[j] < 0) { //当i和j有一个人挣钱有一个人欠钱
            nums[j] += nums[i]; //我们将nums[i]的债务都加到nums[j]的头上,如果nums[i]>0那么就是i帮j还钱,否则是j帮i还钱
            count = Math.min(count, dfs(nums, i + 1) + 1); //然后我们在上边交易的基础上遍历下一个人,+1代表上面一行的交易
            nums[j] -= nums[i]; //尝试所有可能的结果,将j恢复原状态
        }
    }
    return count; //返回可能的最小交易次数
}
// 核心思想就是,shuffle以后两两配对还钱交易,shuffle 1000次,找最小的交易次数
public int minTransfers(int[][] transactions) {
    if(transactions == null || transactions.length == 0) return 0;
    Map<Integer, Integer> acc = new HashMap<>();
    for(int i = 0;i<transactions.length;i++){ //计算每个人的净收入(可正可负)
        int id1 = transactions[i][0];
        int id2 = transactions[i][1];
        int m = transactions[i][2];
        acc.put(id1, acc.getOrDefault(id1, 0)-m);
        acc.put(id2, acc.getOrDefault(id2, 0)+m);
    }
    List<Integer> negs = new ArrayList<>();
    List<Integer> poss = new ArrayList<>();
    for(Integer key:acc.keySet()){ //把收入按正负分成两个集合list
        int m = acc.get(key);
        if(m == 0) continue;
        if(m<0) negs.add(-m);
        else poss.add(m);
    }
    int ans = Integer.MAX_VALUE;
    Stack<Integer> stNeg = new Stack<>(), stPos = new Stack<>();
    for(int i =0;i<1000;i++){ //shuffle 1000次
        for(Integer num:negs) stNeg.push(num);
        for(Integer num:poss) stPos.push(num);
        int cur = 0;
        while(!stNeg.isEmpty()){  //当所有欠钱的人都还清,while循环才终止
            int n = stNeg.pop();
            int p = stPos.pop();
            cur++;
            if(n == p) continue;
            if(n>p){
                stNeg.push(n-p);
            } else {
                stPos.push(p-n);
            }
        }
        ans = Math.min(ans, cur);
        Collections.shuffle(negs); //注意shuffle的是两个list,不是stack,shuffle完以后重新配对
        Collections.shuffle(poss);
    }
    return ans;
}

80.(1165:Easy)Single-Row Keyboard

  • 用hashmap存储每个字符对应的按键索引即可

81.(755:Medium)(非常经典题)Pour Water

  • 首先我们尝试向左走,找到第一个局部最低点,停止条件是左边的高度大于当前高度,但是为了防止出现大家高度都一样而需要停止在靠近起始点位置的情况,我们来一个回滚操作,就是只要和右边的高度一样,就一直往右滚,水滴应该放置在离k最近的位置上。同样,在尝试向右走,找第一个局部最低点,停止条件是右边的高度大于当前高度,但是为了防止出现大家高度都一样而需要停止在靠近起始点位置的情况,我们也来一个回滚操作,就是只要和左边的高度一样,就一直往左滚。
  • 参考博客:博客思路讲解
public int[] pourWater(int[] heights, int V, int K) {
    for(int i = 0; i < V; i++) {
        int cur = K;
        // Move left
        while(cur > 0 && heights[cur] >= heights[cur - 1]) { //注意是等于号
            cur--;
        }
        // Move right
        while(cur < heights.length - 1 && heights[cur] >= heights[cur + 1]) { //注意是等于号
            cur++;
        }
            
        /*
        当海拔高度为22211111时,K=2,那么水滴应停留在K=3处,此时三个while循环都被运行
                    ^
        */
            
        // Move left to K
        while(cur > K && heights[cur] >= heights[cur - 1]) {  //注意是等于号
            cur--;
        }
        heights[cur]++;
    }
    return heights;
}

82.(1166:Medium)(非常经典题)Design File System

  • 将路径及value存入一个HashMap中即可,使用lastIndexOf()找到/的最后位置,然后再map中查找其前缀路径是否存在
Map<String, Integer> file = new HashMap<>(); 
    
public FileSystem() {
    file.put("", -1);
}
    
public boolean createPath(String path, int value) {
    int idx = path.lastIndexOf("/");
    String parent = path.substring(0, idx);
    if (!file.containsKey(parent)) { return false; }
    return file.putIfAbsent(path, value) == null;   
}
    
public int get(String path) {
    return file.getOrDefault(path, -1);
}

83.(759:Hard)(非常非常经典题)Employee Free Time

  • 类似于merge intervals,但该题是寻找大家都有空的common free intervals,思路类似,具体可参看下边的代码及注释
// Definition for an Interval.
class Interval {
    public int start;
    public int end;

    public Interval() {}

    public Interval(int _start,int _end) {
        start = _start;
        end = _end;
    }
};

public List<Interval> employeeFreeTime(List<List<Interval>> avails) {
    List<Interval> result = new ArrayList<>();
    List<Interval> timeLine = new ArrayList<>();
    avails.forEach(e -> timeLine.addAll(e));
    Collections.sort(timeLine, ((a, b) -> a.start - b.start));

    Interval temp = timeLine.get(0);
    for(Interval each : timeLine) {
        if(temp.end < each.start) {
            result.add(new Interval(temp.end, each.start));
            temp = each;
        }else{
            temp = temp.end < each.end ? each : temp;  //选择结束时间靠后的interval作为参照物
        }
    }
    return result;
}

84.(1135:Medium)(非常非常经典题)Connecting Cities With Minimum Cost

  • We use Kruskal’s algorithm to generate a minimum spanning tree for the graph. Use Union-Find to detect cycle. Idea is simple:
  1. Sort edges to no-descresing order
  2. Pick the smallest edge that does not form a cycle
  3. Repeat until MST is formed and every node is connected.

85.(666:Medium)(非常非常经典题)Path Sum IV

  • How do we solve problem like this if we were given a normal tree? Yes, traverse it, keep a root to leaf running sum. If we see a leaf node (node.left == null && node.right == null), we add the running sum to the final result.
  • Now each tree node is represented by a number. 1st digits is the level, 2nd is the position in that level (note that it starts from 1 instead of 0). 3rd digit is the value. We need to find a way to traverse this tree and get the sum.
  • The idea is, we can form a tree using a HashMap. The key is first two digits which marks the position of a node in the tree. The value is value of that node. Thus, we can easily find a node's left and right children using math.
  • Formula: For node xy? its left child is (x+1)(y*2-1)? and right child is (x+1)(y*2)? Given above HashMap and formula, we can traverse the tree. Problem is solved!
    int sum = 0;
    Map<Integer, Integer> tree = new HashMap<>(); //key是level和该level的position(两个digit),value是这个node的值
    
    public int pathSum(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        
        for (int num : nums) {
            int key = num / 10;
            int value = num % 10;
            tree.put(key, value);
        }
        
        traverse(nums[0] / 10, 0);
        
        return sum;
    }
    
    private void traverse(int root, int preSum) {
        int level = root / 10;
        int pos = root % 10;
        int left = (level + 1) * 10 + pos * 2 - 1; //左子树根结点的key
        int right = (level + 1) * 10 + pos * 2; //右子树根节点的key
        
        int curSum = preSum + tree.get(root);
        
        if (!tree.containsKey(left) && !tree.containsKey(right)) { //该结点是叶子结点
            sum += curSum;
            return;
        }
        
        if (tree.containsKey(left)) traverse(left, curSum);
        if (tree.containsKey(right)) traverse(right, curSum);
    }

86.(1153:Hard)(非常非常经典题)String Transforms Into Another String

  • Scan s1 and s2 at the same time, record the transform mapping into a map/array. The same char should transform to the same char. Otherwise we can directly return false.
  • To realise the transformation:
  1. transformation of link link ,like a -> b -> c:

    we do the transformation from end to begin, that is b->c then a->b

  2. transformation of cycle, like a -> b -> c -> a:

    in this case we need a tmp, c->tmp, b->c a->b and tmp->a

    Same as the process of swap two variable.

public boolean canConvert(String s1, String s2) {
    if (s1.equals(s2)) return true;
    Map<Character, Character> dp = new HashMap<>();
    for (int i = 0; i < s1.length(); ++i) {
        if (dp.getOrDefault(s1.charAt(i), s2.charAt(i)) != s2.charAt(i)) // 一个字母只能映射一个不同的字母,可以被多个字母映射,但不能映射多个字母
            return false;
        dp.put(s1.charAt(i), s2.charAt(i)); //存储映射关系
    }
    return new HashSet<Character>(dp.values()).size() < 26; //当映射字母为26个时,我们无法完成映射,无借用字符
}

87.(1056:Easy)(经典题)Confusing Number

  • Similar to 246. Strobogrammatic Number
  • 将有效的数字及其rotation后的结果存入map中,然后依次遍历原来数字的digit即可

88.(1088:Hard)(非常经典题)Confusing Number II

  • The basic idea is to search all numbers composed of 0, 1, 6, 8, 9 and check each of them before they grow bigger than N
    Map<Integer, Integer> map;
    int N;
    int res;
    public int confusingNumberII(int N) {

        map = new HashMap<>();
        map.put(0, 0);
        map.put(1, 1);
        map.put(6, 9);
        map.put(8, 8);
        map.put(9, 6);
        res = 0;
        if (N == 1000000000) {
            res++;
            N--;
        }
        
        this.N = N;
        search(0, 0);
        return res;
    }
    
    //cur永远是由0、1、6、8、9这些有效数字拼接起来的数字
    //p代表当前的digits位数,cur代表当前数字
    //下面的函数遍历所有比N小的且由0、1、6、8、9组成的数字,判断他们是否有效并计数
    private void search(int p, int cur) {
        if (p > 9 || cur > N) {
            return;
        }
        if (isConfusing(cur)) {
            res++;
        }
        for (Integer d : map.keySet()) {
            if (p == 0 && d == 0) {
                continue;
            }
            search(p + 1, cur * 10 + d); //cur是由0、1、6、8、9组成的,且cur*10+d也是由0、1、6、8、9组成的
        }
    }
    
    private boolean isConfusing(int n) {
        long rot = 0;
        int tmp = n;
        while (n > 0) {
            if(!map.containsKey(n % 10))
                return false;
            rot = rot * 10 + map.get(n % 10);
            n /= 10;
        }
        return rot != tmp;
    }

89.(1197:Hard)(非常经典题)Minimum Knight Moves

  • BFS求解即可

90.(727:Hard)(非常非常经典题)Minimum Window Subsequence

  • 这道题给了我们两个字符串S和T,让我们找出S的一个长度最短子串W,使得T是W的子序列,如果长度相同,取起始位置靠前的。清楚子串和子序列的区别,那么题意就不难理解,题目中给的例子也很好的解释了题意。我们经过研究可以发现,返回的子串的起始字母和T的起始字母一定相同,这样才能保证最短。那么你肯定会想先试试暴力搜索吧,以S中每个T的起始字母为起点,均开始搜索字符串T,然后维护一个子串长度的最小值。如果是这种思路,那么还是趁早打消念头吧,博主已经替你试过了,OJ 不依。原因也不难想,假如S中有大量的连续b,并且如果T也很长的话,这种算法实在是不高效啊。根据博主多年经验,这种玩字符串且还是 Hard 的题,十有八九都是要用动态规划 Dynamic Programming 来做的,那么就直接往 DP 上去想吧。DP 的第一步就是设计 dp 数组,像这种两个字符串的题,一般都是一个二维数组,想想该怎么定义。确定一个子串的两个关键要素是起始位置和长度,那么我们的 dp 值到底应该是定起始位置还是长度呢?That is a question! 仔细想一想,其实起始位置是长度的基础,因为我们一旦知道了起始位置,那么当前位置减去起始位置,就是长度了,所以我们 dp 值定为起始位置。那么 dp[i][j] 表示范围S中前i个字符包含范围T中前j个字符的子串的起始位置,注意这里的包含是子序列包含关系。然后就是确定长度了,有时候会使用字符串的原长度,有时候会多加1,看个人习惯吧,这里博主长度多加了个1。
  • OK,下面就是重中之重啦,求状态转移方程。一般来说,dp[i][j] 的值是依赖于之前已经求出的dp值的,在递归形式的解法中,dp数组也可以看作是记忆数组,从而省去了大量的重复计算,这也是 dp 解法凌驾于暴力搜索之上的主要原因。牛B的方法总是最难想出来的,dp 的状态转移方程就是其中之一。在脑子一片浆糊的情况下,博主的建议是从最简单的例子开始分析,比如 S = "b", T = "b", 那么我们就有 dp[1][1] = 0,因为S中的起始位置为0,长度为1的子串可以包含T。如果当 S = "d", T = "b",那么我们有 dp[1][1] = -1,因为我们的dp数组初始化均为 -1,表示未匹配或者无法匹配。下面来看一个稍稍复杂些的例子,S = "dbd", T = "bd",我们的dp数组是:
   ∅  b  d
∅  ?  ?  ?
d  ? -1 -1
b  ?  1 -1
d  ?  1  1
  • 这里的问号是边界,我们还不知道如何初给边界赋值,我们看到,为 -1 的地方是对应的字母不相等的地方。我们首先要明确的是 dp[i][j] 中的j不能大于i,因为T的长度不能大于S的长度,所以j大于i的 dp[i][j] 一定都是-1的。再来看为1的几个位置,首先是 dp[2][1] = 1,这里表示db包含b的子串起始位置为1,make sense!然后是 dp[3][1] = 1,这里表示 dbd 包含b的子串起始位置为1,没错!然后是 dp[3][2] = 1,这里表示 dbd 包含 bd 的起始位置为1,all right! 那么我们可以观察出,当 S[i] == T[j] 的时候,实际上起始位置和 dp[i - 1][j - 1] 是一样的,比如 dbd 包含 bd 的起始位置和 db 包含b的起始位置一样,所以可以继承过来。那么当 S[i] != T[j] 的时候,怎么搞?其实是和 dp[i - 1][j] 是一样的,比如 dbd 包含b的起始位置和 db 包含b的起始位置是一样的
  • 嗯,这就是状态转移方程的核心了,下面再来看边界怎么赋值,由于j比如小于等于i,所以第一行的第二个位置往后一定都是-1,我们只需要给第一列赋值即可。通过前面的分析,我们知道了当 S[i] == T[j] 时,我们取的是左上角的 dp 值,表示当前字母在S中的位置,由于我们dp数组提前加过1,所以第一列的数只要赋值为当前行数即可。最终的 dp 数组如下:
   ∅  b  d
∅  0 -1 -1
d  1 -1 -1
b  2  1 -1
d  3  1  1
  • 为了使代码更加简洁,我们在遍历完每一行,检测如果 dp[i][n] 不为-1,说明T已经被完全包含了,且当前的位置跟起始位置都知道了,我们计算出长度来更新一个全局最小值 minLen,同时更新最小值对应的起始位置 start,最后取出这个全局最短子串,如果没有找到返回空串即可,具体实现请参看代码

  • 除了DP求解,还有另外双指针的思路。论坛上的 danzhutest大神 提出了一种双指针的解法,其实这是优化过的暴力搜索的方法,而且居然 beat 了 100%,给跪了好嘛?!而且这双指针的跳跃方式犹如舞蹈般美妙绝伦,比那粗鄙的暴力搜索双指针不知道高到哪里去了?!举个栗子来说吧,比如当 S = "bbbbdde", T = "bde" 时,我们知道暴力搜索的双指针在S和T的第一个b匹配上之后,就开始检测S之后的字符能否包含T之后的所有字符,当匹配结束后,S的指针就会跳到第二个b开始匹配,由于有大量的重复b出现,所以每一个b都要遍历一遍,会达到平方级的复杂度,会被 OJ 无情拒绝。而下面这种修改后的算法会跳过所有重复的b,使得效率大大提升,具体是这么做的,当第一次匹配成功后,我们的双指针往前走,找到那个刚好包含T中字符的位置,比如开始指针 i = 0 时,指向S中的第一个b,指针 j = 0 时指向T中的第一个b,然后开始匹配T,当 i = 6, j = 2 时,此时完全包含了T。暴力搜索解法中此时i会回到1继续找,而这里,我们通过向前再次匹配T,会在 i = 3,j = 0 处停下,然后继续向后找,这样S中重复的b就会被跳过,从而大大的提高了效率,但是最坏情况下的时间复杂度还是 O(mn)。具体思路请参看代码实现。
⚠️ **GitHub.com Fallback** ⚠️