刷题篇--热题HOT 11-20

20.有效的括号

 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足:
 左括号必须用相同类型的右括号闭合。
 左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
输入: "()",输出: true;输入: "()[]{}",输出: true;输入: "(]",输出: false;输入: "([)]",输出: false;输入: "{[]}",输出: true;
分析:第一反应是使用栈,遇到左括号压入,遇到右括号弹出并比较.

 1 class Solution {
 2     public boolean isValid(String s) {
 3         if(s.length()%2==1) return false;
 4         Stack<Character> brackets = new Stack();
 5         Map<Character,Character> map = new HashMap();
 6         map.put(')', '(');
 7         map.put(']', '[');
 8         map.put('}', '{');
 9         int countOfLeft=0,countOfRight=0;//计算左括号和右括号数量
10         char[] chs = s.toCharArray();
11         for(int i=0; i < chs.length; i++){
12             if(chs[i] == '(' || chs[i] == '[' || chs[i] == '{'){
13                 countOfLeft++;
14                 brackets.push(chs[i]);
15             }else if(chs[i] == ')' || chs[i] == ']' || chs[i] == '}'){
16                 countOfRight++;
17                 if(brackets.empty() || brackets.pop()!=map.get(chs[i])) return false;//如果括号栈为空或者弹出的不是正确左括号,则false
18             }
19         }
20         return countOfLeft==countOfRight;//如果左括号数和右括号数量不相等则false
21     }
22 }

 

21.合并两个有序链表

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:输入:1->2->4, 1->3->4.输出:1->1->2->3->4->4。

 分析:新建头节点(解决链表问题需要注意这个小Tips)

 1 /**
 2  * Definition for singly-linked list.
 3  * public class ListNode {
 4  *     int val;
 5  *     ListNode next;
 6  *     ListNode(int x) { val = x; }
 7  * }
 8  */
 9 class Solution {
10     public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
11         ListNode preNode = new ListNode(-1);
12         ListNode resNode = preNode;
13         while(l1 != null && l2 != null){
14             if(l1.val > l2.val){
15                 preNode.next = l2;
16                 l2 = l2.next;
17             }else{
18                 preNode.next = l1;
19                 l1 = l1.next;
20             }
21             preNode = preNode.next;
22         }
23         preNode.next = l1==null ? l2 : l1;
24         return resNode.next;
25     }
26 }

 

 22.括号生成

给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。

 例如,给出 n = 3,生成结果为:
[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

分析:n是括号的对数,即有n个左括号和n个右括号,当从前往后添加括号字符时,需要注意右括号的数量不能比左括号多,例如 ())…(因此第一个加入的肯定是左括号),并且左括号的数量不能超过n个。

  综上:添加括号字符时,只要保证左括号数目小于等于n,右括号数目小于等于左括号数目这两个条件就可以了。(从0开始计数)

 

 1 class Solution {
 2     List<String> list = new ArrayList();
 3     public List<String> generateParenthesis(int n) {
 4         String s = "";
 5         int numOfLeft = 0, numOfRight = 0;
 6         addParenthesis(n, s, numOfLeft, numOfRight);
 7         return list;
 8     }
 9     public void addParenthesis(int n, String s, int numOfLeft, int numOfRight){
10         //递归结束条件
11         if(s.length() == 2*n){
12             list.add(s);
13             return;
14         }
15         if(numOfLeft < n){
16             addParenthesis(n, s+'(', numOfLeft+1, numOfRight);
17         }
18         if(numOfRight < numOfLeft){
19             addParenthesis(n, s+')', numOfLeft, numOfRight+1);
20         }
21     }
22 }

 

 

 23.合并K个排序列表

合并 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

 输入: [   1->4->5,   1->3->4,   2->6 ] 输出: 1->1->2->3->4->4->5->6

分析:21题我们做过合并两个链表,这里是合并K个链表,如何将两个联系起来?可以想到将第一个链表和第K个链表合并,第二个链表和第K-1个链表合并,一轮之后只剩下K/2个,再次执行此操作,直至K=1。时间复杂度:O(NK)

 1 /**
 2  * Definition for singly-linked list.
 3  * public class ListNode {
 4  *     int val;
 5  *     ListNode next;
 6  *     ListNode(int x) { val = x; }
 7  * }
 8  */
 9 class Solution {
10     public ListNode mergeKLists(ListNode[] lists) {
11         int k = lists.length;
12         if(k == 0) return null;
13         while(k > 1){
14             for(int i=0; i < k/2; i++){
15                 lists[i] = merge2Lists(lists[i], lists[k-1-i]);
16             }
17             k = (k+1)/2;
18         }
19         return lists[0];
20     }
21     public ListNode merge2Lists(ListNode l1, ListNode l2){
22         ListNode newPre = new ListNode(-1);
23         ListNode res = newPre;
24         while(l1 != null && l2 != null){
25             if(l1.val > l2.val){
26                 newPre.next = l2;
27                 l2 = l2.next;
28             }else{
29                 newPre.next = l1;
30                 l1 = l1.next;
31             }
32             newPre = newPre.next;
33         }
34         newPre.next = l1 == null ? l2 : l1;
35         return res.next;
36     }
37 }

 

 

31.下一个排列

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。必须原地修改,只允许使用额外常数空间。
以下是一些例子,输入位于左侧列,其相应输出位于右侧列。
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

分析:下一个更大的排列:即从后往前找,看看后面有没有比前边一个数大的,如果有,则排列成最小数列。如果遍历完整个数组都没有找到符合条件的,则该数组是一个降序数组,此时应该考虑将数组转换成升序数组。

nums = [1,5,6,4,7,6,5,3,1]  => 当判决到 4和7 时,发生改变,需要将4和4后面与4差值(比4大)最小的数5交换位置,即分成1,5,6,5和7,6,4,3,1两个部分,前一部分保证比之前数列值大,后一部分保证仅比之前数列大一点点(保证"下一个")。因此对后一部分还要进行升序排序。

 1 class Solution {
 2     public void nextPermutation(int[] nums) {
 3         int len = nums.length;
 4         //找更大排列分界点
 5         int i = len-1;
 6         while(i>0 && nums[i]<=nums[i-1]){////如果跳出循环,即nums[i-1]比nums[i]小,4<7,i-1即是4的索引
 7             i--;
 8         }
 9         i = i-1;//i为分界点,即4的索引,如果i=-1表示没有,即原数组是一个升序数组
10         if(i>=0){
11             int j = len-1;
12             while(j > i && nums[j] <= nums[i]){
13                 j--;//找出5的位置索引
14             }
15             swap(i,j,nums);//将4和5交换位置
16         }
17         //将4之后的数列降序处理或者整个数列进行降序处理
18         //如果执行到此步说明没有更大数列,该数列为降序,现要改成升序
19         reverse(nums, i+1);
20     }
21     public void reverse(int[] nums, int start){
22         int i = start, j = nums.length-1;
23         while(i<j){
24             swap(i, j, nums);
25             i++;
26             j--;
27         }
28     }
29     public void swap(int i, int j, int[] nums){
30         int tmp = nums[i];
31         nums[i] = nums[j];
32         nums[j] = tmp;
33     }
34 }

 

 

32.最长有效括号

给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。输入: "(()" 输出: 2 解释: 最长有效括号子串为 "()";输入: ")()())" 输出: 4 解释: 最长有效括号子串为 "()()"

分析:使用栈,利用字符在字符串中的索引来计算最大长度。

例如 :首先压入-1(便于计算最大长度),

    遇到‘(’就压入索引,

    遇到‘)’就弹出,

      如果弹出后栈不为空则当前最大长度为(当前索引-栈顶元素),并与总最大长度比较。

      如果弹出之后栈为空,说明之前的都弹完了,则压入当前索引,作为计算下一次最大长度初始值使用。

 

 

 

 

 

 

=》

 

 

 

 

 

 

 

 

 

=》

 

 

 

 

 

 

 

 

 

 

=》

 

 

 

 

 

 

 

 

 

=》

 

 

 

 

 

 1 class Solution {
 2     public int longestValidParentheses(String s) {
 3         Stack<Integer> stack = new Stack();
 4         stack.push(-1);
 5         int maxLen = 0;
 6         for(int i=0; i<s.length(); i++){
 7             if(s.charAt(i) == '('){
 8                 stack.push(i);
 9             }else{
10                 stack.pop();
11                 if(stack.empty()){
12                     stack.push(i);
13                 }else{
14                     maxLen = Math.max(maxLen, i-stack.peek());
15                 }
16             }
17         }
18         return maxLen;
19     }
20 }

 

 

33.搜索旋转排序数组

假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。你可以假设数组中不存在重复的元素。你的算法时间复杂度必须是 O(log n) 级别。

示例 1:输入: nums = [4,5,6,7,0,1,2], target = 0输出: 4
示例 2:输入: nums = [4,5,6,7,0,1,2], target = 3输出: -1

分析:由题目中给出的复杂度可以得知使用二分查找,不断缩小查找范围。通过半段有序部分来划分(也可以先二分查找分界点,然后通过分界点来划分,但是要进行两次二分查找,较为麻烦)。

  如果nums[start] <= nums[mid], 例如 2,3,4,5,6,7,0,1    2<5,则说明start~mid的元素是有序的

    如果nums[start] < target < nums[mid],就在start~mid这个范围找,反之,则在后半部分查找。

  如果nums[start] >= nums[mid], 例如 5,6,7,1,2,3,4,   5>1,则说明mid~end的元素是有序的

    如果nums[mid] < target < nums[end],就在mid~end这个范围找,反之,则在前半部分查找。

 1 class Solution {
 2     public int search(int[] nums, int target) {
 3         int start =0, end = nums.length-1;
 4         int mid;
 5         while(start <= end){
 6             mid = (start+end)/2;
 7             if(nums[mid]==target) return mid;
 8             if(nums[start] <= nums[mid]){
 9                 if(nums[start] <= target && target < nums[mid]){
10                     end = mid - 1;
11                 }else{
12                     start = mid + 1;
13                 }
14             }else{
15                 if(nums[mid] < target && target <= nums[end]){
16                     start = mid +1;
17                 }else{
18                     end = mid-1;
19                 }
20             }
21         }
22         return -1;
23     }
24 }

 

 

34.在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。你的算法时间复杂度必须是 O(log n) 级别。如果数组中不存在目标值,返回 [-1, -1]。

示例 1:输入: nums = [5,7,7,8,8,10], target = 8 输出: [3,4]

示例 2:输入: nums = [5,7,7,8,8,10], target = 6 输出: [-1,-1]

分析:初始值left = 0, right = len-1,mid = (left + right)/2。target与mid比较。因为本题不只是找一个,则需要分别查找左边索引和右边索引,编写两个函数。

 1 class Solution {
 2     public int[] searchRange(int[] nums, int target) {
 3         return new int[]{searchLeft(nums, target), searchRight(nums, target)};
 4     }
 5     //获取左边索引
 6     public int searchLeft(int[] nums, int target){
 7         int left = 0, right = nums.length-1, mid;
 8         while(left <= right){
 9             mid = (left + right)/2;
10             if(nums[mid] == target){//当mid位置值等于目标值时
11                 if(mid==0 || nums[mid-1] != target){//判断是否再向前进一步查询
12                     return mid;
13                 }else{
14                     right = mid-1;//此时通过上面判断,mid-1位置依然等于target,因此进一步向左查询
15                 }
16             }else if(nums[mid] > target ){
17                 right = mid - 1;
18             }else{
19                 left = mid + 1;
20             }
21         }
22         return -1;
23     }
24     //获取右边索引
25     public int searchRight(int[] nums, int target){
26         int left = 0, right = nums.length-1, mid;
27         while(left <= right){
28             mid = (left + right)/2;
29             if(nums[mid] == target){//当mid位置值等于目标值时
30                 if(mid==nums.length-1 || nums[mid+1] != target){//判断是否再向前进一步查询
31                     return mid;
32                 }else{
33                     left = mid+1;//此时通过上面判断,mid+1位置依然等于target,因此进一步向右查询
34                 }
35             }else if(nums[mid] > target ){
36                 right = mid - 1;
37             }else{
38                 left = mid + 1;
39             }
40         }
41         return -1;
42     }
43 }

 

 

39.组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。所有数字(包括 target)都是正整数。解集不能包含重复的组合。

示例 1:输入: candidates = [2,3,6,7], target = 7,所求解集为:[ [7], [2,2,3]]
示例 2:输入: candidates = [2,3,5], target = 8,所求解集为:[  [2,2,2,2], [2,3,3], [3,5]]

分析:排列组合,类似于剑指Offer里字符串排列

 1 class Solution {
 2     List<List<Integer>> res = new ArrayList();
 3     public List<List<Integer>> combinationSum(int[] candidates, int target) {
 4         Arrays.sort(candidates);//排序,方便去重
 5         List<Integer> list = new ArrayList();
 6         findCombination(candidates, target, 0, list);
 7         return res;
 8     }
 9     public void findCombination(int[] candidates, int target, int index, List<Integer> list){
10         if(target < 0){
11             return;
12         } 
13         if(target == 0){
14             res.add(new ArrayList(list));
15             return;
16         }
17         for(int i=index; i<candidates.length; i++){
18             list.add(candidates[i]);
19             findCombination(candidates, target-candidates[i], i, list);
20             list.remove(list.size()-1);
21         }
22     }
23 }

 

 42.接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

 

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。

示例:输入: [0,1,0,2,1,0,1,3,2,1,2,1]输出: 6

分析:当前柱子的接水量等于(min{左边最高柱子高度,右边最高柱子高度} - 当前高度),问题的关键就是如何找到并存储左右最高柱子高度值。

方法一:暴力寻找,每次遍历i时,再次遍历i左边,寻找左边最高柱子,然后再次遍历右边,寻找右边最高柱子。循环嵌套,时间复杂度O(N^2) 空间复杂度 O(1)

方法二:先从左往右遍历,使用数组存储左边最高值,再从右往左遍历,使用数组存储右边最高值。再从左往右遍历,计算接水量。时间复杂度O(N) 空间复杂度 O(N)。

 1 class Solution {
 2     public int trap(int[] height) {
 3         int len = height.length, res = 0;
 4         if(len == 0) return 0;
 5         //左边最高块
 6         int[] maxLeft = new int[len];
 7         maxLeft[0] = height[0];
 8         for(int i = 1; i < len; i++){
 9             maxLeft[i] = Math.max(maxLeft[i-1], height[i]);
10         }
11 
12         //右边最高块
13         int[] maxRight = new int[len];
14         maxRight[len-1] = height[len-1];
15         for(int i = len-2; i >= 0; i--){
16             maxRight[i] = Math.max(maxRight[i+1], height[i]);
17         }
18 
19         //计算res
20         for(int i = 0; i < len; i++){
21             res += Math.min(maxLeft[i], maxRight[i]) - height[i];
22         }
23 
24         return res;
25         
26     }
27 }

 

方法三:双指针法,通过分析可以知道以最高块为分界线(i=7),左边(0~6)接水量由左边最高值决定,右边(7~11)接水量由右边最高值决定.

那么如何在遍历中区分当前位置在左边还是右边呢?此时使用双指针,默认右边边界持有最高值,则当height[left] >= height[right]时,right指针就要向左移动,寻找比height[left]更大的值,反之,left指针向右移动,这个过程中双指针不断地向最高柱子移动,如果一个已经先到了,则会停止。时间复杂度O(N) 空间复杂度 O(1)

  具体步骤如下:

  while left < right

    if height[left] < height[right]

      if height[left] >= maxLeft

        maxLeft = height[left]   //更新左边最大值

      else

        res +=  maxLeft - height[left]  //计算结果

      left++

    else

      if height[right] >= maxright

        maxright = height[right]   //更新右边最大值

      else

        res +=  maxLeft - height[left]  //计算结果

      right --

 1 class Solution {
 2     public int trap(int[] height) {
 3         int left = 0, right = height.length-1, maxLeft = 0, maxRight = 0, res = 0;
 4         while(left < right){
 5             if(height[left] < height[right]){
 6                 if(height[left] >= maxLeft){
 7                     maxLeft = height[left];
 8                 }else{
 9                     res += maxLeft - height[left];
10                 }
11                 left++;
12             }else{
13                 if(height[right] >= maxRight){
14                     maxRight = height[right];
15                 }else{
16                     res += maxRight - height[right];
17                 }
18                 right--;
19             } 
20         }
21         return res;
22     }
23 }
posted @ 2019-12-30 10:25  Qmillet  阅读(...)  评论(...编辑  收藏