LeetCode 热题 HOT 100 打卡计划 | 第五天 | 每日进步一点点

102 阅读4分钟

图片.png

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

22. 括号生成

思路

(dfs) O(C2nn)O(C_{2n}^{n})

首先我们需要知道一个结论,一个合法的括号序列需要满足两个条件:

  • 1、左右括号数量相等
  • 2、任意前缀中左括号数量 >= 右括号数量 (也就是说每一个右括号总能找到相匹配的左括号)

图片.png

题目要求我们生成n对的合法括号序列组合,可以考虑使用深度优先搜索,将搜索顺序定义为枚举序列的每一位填什么,那么最终的答案一定是有n个左括号和n个右括号组成。

如何设计dfs搜索函数?

最关键的问题在于搜索序列的当前位时,是选择填写左括号,还是选择填写右括号 ?因为我们已经知道合法的括号序列任意前缀中左括号数量一定 >= 右括号数量,因此,如果左括号数量不大于 n,我们可以放一个左括号,等待一个右括号来匹配 。如果右括号数量小于左括号的数量,我们可以放一个右括号,来使一个右括号和一个左括号相匹配。

递归树如下:

图片.png

递归函数设计

 void dfs(int n ,int lc, int rc ,string str)

n是括号对数,lc是左括号数量,rc是右括号数量,str是当前维护的合法括号序列。

搜索过程如下:

  • 1、初始时定义序列的左括号数量lc 和右括号数量rc都为0
  • 2、如果 lc < n,左括号的个数小于n,则在当前序列str后拼接左括号。
  • 3、如果 rc < n && lc > rc , 右括号的个数小于左括号的个数,则在当前序列str后拼接右括号。
  • 4、当lc == n && rc == n 时,将当前合法序列str加入答案数组res中。

时间复杂度分析: 经典的卡特兰数问题,因此时间复杂度为 O(1n+1C2nn)=O(C2nn)O(\frac{1}{n+1}C_{2n}^{n}) = O(C_{2n}^n)

c++代码

 class Solution {
 public:
     vector<string> res;
     vector<string> generateParenthesis(int n) {
         dfs(n, 0, 0, "");
         return res;
     }
 ​
     void dfs(int n, int lc, int rc, string str){
         if(lc == n && rc == n){
             res.push_back(str);
             return ;
         }
         if(lc < n) dfs(n, lc + 1, rc, str + '(');
         if(rc < n && lc > rc) dfs(n, lc, rc + 1, str + ')');
     }
 };

23. 合并K个升序链表

思路

(优先队列) O(nlogk)O(nlogk)

我们可以通过双路归并合并两个有序链表,但是这题要求对多个链表进行并操作。 其实和双路归并思路类似,我们分别用指针指向该链表的头节点,每次找到这些指针中值最小的节点,然后依次连接起来,并不断向后移动指针。

如何找到一堆数中的最小值?

用小根堆维护指向k个链表当前元素最小的指针,因此这里我们需要用到优先队列,并且自定义排序规则,如下:

 struct cmp{ //自定义排序规则
     bool operator() (ListNode* a, ListNode* b){
         return a->val > b->val;  // val值小的在队列前
     }
 };

具体过程如下:

  • 1、定义一个优先队列,并让val值小的元素排在队列前。

  • 2、新建虚拟头节点dummy,定义 cur指针并使其指向 dummy

  • 3、首先将k个链表的头节点都加入优先队列中。

  • 4、当队列不为空时:

    • 取出队头元素t(队头即为k个指针中元素值最小的指针);
    • curnext 指针指向t,并让cur后移一位;
    • 如果tnext指针不为空,我们将t->next加入优先队列中;
  • 5、最后返回dummy->next

时间复杂度分析: O(nlogk)O(nlogk),n表示的是所有链表的总长度,k表示k个排序链表。

c++代码

 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     struct cmp{ //自定义排序规则
         bool operator() (ListNode* a, ListNode* b){
             return a->val > b->val;  // val值小的在队列前
         }
     };
     ListNode* mergeKLists(vector<ListNode*>& lists) {
         if(lists.size() == 0) return nullptr;
         priority_queue<ListNode*, vector<ListNode*>, cmp> heap;
         auto dummy = new ListNode(-1), cur = dummy;
         for(ListNode* l : lists)  if(l) heap.push(l);
         while(heap.size()){
             ListNode* t = heap.top();  // k个指针中元素值最小的指针t取出来
             heap.pop(); 
             cur = cur->next = t;
             if(t->next)  heap.push(t->next);  //将t->next加入优先队列中
         } 
         return dummy->next;
     }
 };
 ​

31. 下一个排列

思路

(找规律) O(n)O(n)

对于数组排列问题,我们可以知道,如果一个数组是升序数组,那么它一定是最小的排列。如果是降序数组,那么它一定是最大的排列。

而找下一个排列就是从后往前寻找第一个出现降序的地方,把这个地方的数字与后边第一个比它大的的数字交换,再把该位置之后整理为升序。

换句话说,就是为了从后往前找,找到第一个“ 可以变大的数 “,而从前往后的降序序列已经最大了,因此第一个可以变大的数一定出现在从前往后的升序序列中,即从后往前的第一个降序地方

具体过程如下:

  • 1、从数组末尾往前找,找到 第一个 位置 k,使得 nums[k-1] < nums[k],则从后往前看nums[k-1],nums[k]满足降序,nums[k-1]就是第一个可以变大的数
  • 2、如果k <=0,说明不存在这样的k ,则数组是不递增的,直接将数组逆转即可。
  • 3、如果存在这样的k,则让t = k,从前往后找到第一个位置 t,使得 nums[t] <= nums[k-1],则nums[t-1]就是第一个大于nums[k-1]的数。
  • 4、交换 nums[k-1]nums[t-1],然后将数组从k 到末尾部分逆序。

图示过程

图片.png 时间复杂度分析: 遍历整个数组需要的时间为O(n)O(n)

c++代码

 class Solution {
 public:
     void nextPermutation(vector<int>& nums) {
         int k = nums.size() - 1;
         while(k > 0 && nums[k - 1] >= nums[k]) k--;
         if(k <= 0){  //从前往后降序
             reverse(nums.begin(), nums.end());
         }else{
             int t = k;
             while(t < nums.size() && nums[t] > nums[k - 1]) t++; 
             swap(nums[t - 1], nums[k - 1]);
             reverse(nums.begin() + k, nums.end());
         }
     }
 };