4.10

25. K 个一组翻转链表 - 力扣(LeetCode)

前置题为92. 反转链表 II - 力扣(LeetCode)

92题的题解如下:

/**
 * 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:
      ListNode* reverseBetween(ListNode* head, int l, int r) {
          ListNode dummy(0 , head);
          ListNode* p0 = &dummy;
          //找到l的前一个节点
          for (int i = 0; i < l - 1; i++) {
             p0 = p0->next;
          }

          ListNode* pre = nullptr;
          ListNode* cur = p0->next;
          for (int i = 0; i < r - l + 1; i++) {
             ListNode* nxt = cur->next;
             cur->next = pre;
             pre = cur;
             cur = nxt;
          }
          p0->next->next = cur;
          p0->next = pre;

          return dummy.next;
      }
  };

有两种写法:

  1. 如果定义 ListNode* dummyHead = new ListNode(0,head);那么此时dummyHead是链表指针,最后返回的是dummyHead->next
  2. 如果定义 ListNode dummy(0 , head),那么此时ListNode是链表结构体,最后返回的是dummy.next

image-20241117223328808

image-20241117223428508

image-20241117223450407

image-20250220203850679

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        // 统计节点个数
        int n = 0;
        for (ListNode* cur = head; cur; cur = cur->next) {
            n++;
        }

        ListNode dummy(0, head);
        ListNode* p0 = &dummy;
        ListNode* pre = nullptr;
        ListNode* cur = head;

        // k 个一组处理
        for (; n >= k; n -= k) {
            for (int i = 0; i < k; i++) { // 同 92 题
                ListNode* nxt = cur->next;
                cur->next = pre; // 每次循环只修改一个 next,方便大家理解
                pre = cur;
                cur = nxt;
            }

             // 如图,pre为反转后的头,cur为下一组的头 
          	//比92题多了记下p0->next然后赋值给p0这一步
            ListNode* nxt = p0->next;//记录下翻转前的头,翻转后的末尾
          
            p0->next->next = cur;//接到下一组的头
            p0->next = pre;//p0接到翻转后的头
          
            p0 = nxt;//翻转后的末尾是下一个循环的前驱结点
        }
        return dummy.next;
    }
};

23. 合并 K 个升序链表 - 力扣(LeetCode)

1.最小堆

​ 先把所有链表头放到最小堆里,当堆非空时,每次弹出堆头最小节点node

​ 如果node->next非空,就加入堆

​ 一直循环直到堆空

class Solution {
  public:
      ListNode* mergeKLists(vector<ListNode*>& lists) {
          auto cmp = [](const ListNode* a , const ListNode* b){
            return a->val > b->val;
          };
          priority_queue<ListNode*, vector<ListNode*> , decltype(cmp)> pq;

          for (auto head : lists){
            if(head)  pq.push(head);
          }

          ListNode dummy{};
          auto cur = &dummy;
          while(!pq.empty()){
            auto node = pq.top();
            pq.pop();
            if(node->next)  pq.push(node->next);

            cur->next = node;
            cur = cur->next;
          }
          return dummy.next;
      }
  };

复杂度分析

  • 时间复杂度:O(nlogk),其中k为lists的长度,n为所有链表的节点数之和。
  • 空间复杂度:O(k)。堆中至多有k个元素。

方法二:分治

暴力做法是,按照21. 合并两个有序链表的题解思路,先合并前两个链表,再把得到的新链表和第三个链表合并,再和第四个链表合并,依此类推。

但是这种做法,平均每个节点会参与到O(k)次合并中(用(1+2+⋯+k)/k粗略估计),所以总的时间复杂度为O(nk)

一个巧妙的思路是,把lists一分为二(尽量均分),先合并前一半的链表,再合并后一半的链表,然后把这两个链表合并成最终的链表。如何合并前一半的链表呢?我们可以继续一分为二。如此分下去直到只有一个链表,此时无需合并。

我们可以写一个递归来完成上述逻辑,按照一分为二再合并的逻辑,递归像是在后序遍历一棵平衡二叉树。由于平衡树的高度是O(logk),所以每个链表节点只会出现在O(logk)次合并中!这样就做到了更快的O(nlogk)时间。

假设输入链表为:这个例子中,合并三个链表的分治过程如下:

  • List1: 1 → 4 → 5
  • List2: 1 → 3 → 4
  • List3: 2 → 6

分治步骤

  1. 分割:将三个链表分成左半部分(List1)和右半部分(List2、List3)。
  2. 合并右半部分
    • 进一步分割为 List2 和 List3。
    • 合并 List2 和 List3 → 1 → 2 → 3 → 4 → 6。
  3. 合并左半部分与右半结果
    • 合并 List1(1→4→5)和上一步的结果(1→2→3→4→6)→ 最终合并结果为 1→1→2→3→4→4→5→6。

最终结果
1 → 1 → 2 → 3 → 4 → 4 → 5 → 6

代码执行流程

  • 递归分割链表数组,逐层合并相邻链表,最终将所有链表合并为一个有序链表。
class Solution {
    // 21. 合并两个有序链表
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy{}; // 用哨兵节点简化代码逻辑
        auto cur = &dummy; // cur 指向新链表的末尾
        while (list1 && list2) {
            if (list1->val < list2->val) {
                cur->next = list1; // 把 list1 加到新链表中
                list1 = list1->next;
            } else { // 注:相等的情况加哪个节点都是可以的
                cur->next = list2; // 把 list2 加到新链表中
                list2 = list2->next;
            }
            cur = cur->next;
        }
        cur->next = list1 ? list1 : list2; // 拼接剩余链表
        return dummy.next;
    }

    // 合并从 lists[i] 到 lists[j-1] 的链表
    ListNode* merge(vector<ListNode*>& lists, int i, int j) {
        int m = j - i;
        if (m == 0)   return nullptr; // 注意输入的 lists 可能是空的
                   
        if (m == 1)   return lists[i]; // 无需合并,直接返回
                 
        auto left = merge(lists, i, i + m / 2); // 合并左半部分
        auto right = merge(lists, i + m / 2, j); // 合并右半部分
        return mergeTwoLists(left, right); // 最后把左半和右半合并
    }

public:
    ListNode* mergeKLists(vector<ListNode* > &lists) {
        return mergeKLists(lists, 0, lists.size());
    }
};

复杂度分析

  • 时间复杂度:O(nlogk),其中k为lists的长度,n为所有链表的节点数之和。每个节点参与链表合并的次数为O(logk)次,一共有n个节点,所以总的时间复杂度为O(nlogk)。
  • 空间复杂度:O(logk)。递归深度为O(logk),需要用到O(logk)的栈空间。Python 忽略切片产生的额外空间。

20. 有效的括号 - 力扣(LeetCode)

class Solution {
  bool check(char a , char c){
    if((a == '{' && c =='}') || (a == '(' && c == ')') || (a == '[' && c == ']'))  return true;

    return false;
  }

public:
  bool isValid(string s) {
      stack<char> st;
      for(char c : s){
        if(st.empty() || c == '(' || c == '[' || c == '{'){
          st.push(c);
          continue;
        }
    
        char a = st.top();
        st.pop();        
        if(!check(a ,c))  return false;       
        }         
      if (st.empty())  return true;
      else return false;
  }
};

470. 用 Rand7() 实现 Rand10() - 力扣(LeetCode)

解题思路

移位加法构造

  1. 题目给你一个等概率函数,你可以把他转换为一个等概率返回0和1的函数rand01();

  2. 因为f()可以构造任何等概率的函数,以题目举例;

  3. 已知等概率返回1-7,我们可以在rand01()循环中等于4重新计算,小于4返回0,大于4返回1。这是等概率的。

  4. 构造1-10等概率,可以先构造0-9等概率再加1。估算至少需要4个二进制位,相加即可。(9(10) = 1001(2))。从右向左来看0、1、2、3分别代表是否加1,加2,加4,加8,任何一种可能都是等概率的,所以返回等概率0到15,大于9的重新计算即可。

    PS:此类题以后先转换成等概率返回0和1

// The rand7() API is already defined for you.
// int rand7();
// @return a random integer in the range 1 to 7

class Solution {
    int rand01(){
      int res;
      do{
        res = rand7();
      }
      while(res == 4);
      return res < 4 ? 0 : 1;
    }

  public:
      int rand10() {
          int res;
          do{
            res = (rand01() << 3) + (rand01() << 2) + (rand01() << 1) + rand01();
          }  while(res > 9);
          return res + 1;    
      }
  };

万能构造法:独立随机事件+古典概型

乘法构造

[1,X] 的随机数发生器 randX() 很明显是一个古典概型:它的结果是有限的,且每个结果的概率相同。
独立随机事件的概率:P(AB)=P(A)∗P(B)
那么任意的 randX() 都可以用以下方法构造:

randA() 构造 randB() 时,需要找一个最大质因子不超过 A 的数 n (n>=B),然后对 n 分解质因子就能找到每个采样需要取多少种结果。实际到具体数字时,可以把部分质因子合并成不超过 A 的数,从而减少采样次数。

  1. 构造 n 次相互独立的采样,其中第 i 次采样有 mi 种结果,且第 i 次采样中每种结果的概率是 mi1。n 要满足 m1∗m2∗⋯∗mn≥X ,即把所有采样结果组合起来,最终的结果数量不少于 X,保证可以映射到 [1,X] 的每一个元素。
    这样做的好处是,我们构造了 m1∗m2∗⋯∗mn 个结果,并且每个结果的概率都是 m1∗m2∗⋯∗mn1。
  2. 从 m1∗m2∗⋯∗mn 个结果中取 X 个,映射到 [1,X] 区间,我们就得到了一个均匀分布在 [1,X] 的随机数发生器。

第二步中的映射是 1:1 映射,实际运用中,第二步可以取 k∗X 个结果来做 k:1 映射,以减少调用 rand7() 次数的期望。

rand7() 构造 rand10()

取n = 10 ,n的最大不超过7的质因子为5,因此10= 5 * 2,调用两次rand7()即可。

  1. 构造 2 次采样,分别有 2 和 5 种结果,组合起来便有 10 种概率相同的结果。
  2. 把这 10 种结果映射到 [1,10] 即可。

第一步具体要如何构造采样是自由的,比如 rand7() 拒绝 7,然后对 [1,6] 采样,把奇数和偶数作为 2 种结果,这 2 种结果的概率均为 0.5 。

rand7() 拒绝 6,7 ,然后对 [1,5] 采样,有 5 种结果,每种概率均为 0.2 。
代码如下。

// The rand7() API is already defined for you.
// int rand7();
// @return a random integer in the range 1 to 7

class Solution {
  public:
      int rand10() {
        int a , b;
          while((a = rand7()) > 6);
          while((b = rand7()) > 5);
          return (a & 1) == 1 ? b : b + 5;  //a是奇数则返回1-5,是偶数返回6-10  
      }
  };

rand7() 构造任意范围的随机数发生器

上述方法理论上可以构造任何范围的随机数发生器,比如 rand11() :

  1. 构造 2 次采样,分别有 2 和 6 种结果,组合起来便有 12 种概率相同的结果。
  2. 把这 12 种结果映射到 [1,12] ,然后再拒绝 12 即可。

rand100() :

  1. 构造 3 次采样,分别有 4,5,5 种结果,组合起来便有 100 种概率相同的结果。
  2. 把这 100 种结果映射到 [1,100] 即可。

442. 数组中重复的数据 - 力扣(LeetCode)

解法:原地哈希

这道题要求找出数组 nums 中的所有出现两次的整数并返回。常规的做法是使用哈希表存储数组中的整数,遍历数组并将每个整数存入哈希表,如果遍历到一个元素时发现该元素已经在哈希表中,则该元素是出现两次的整数。对于长度为 n 的数组,使用哈希表的时间复杂度是 O(n),符合题目要求,但是空间复杂度是 O(n),不符合题目要求的常数空间。

为了将空间复杂度降低到常数,不能额外创建哈希表,只能原地修改数组。由于数组 nums 的长度是 n,下标范围是 [0,n−1],每个元素都在范围 [1,n] 内,因此可以将数组看成哈希表,利用数组下标的信息表示每个整数是否出现两次。对于下标 index,满足 0≤index<n,1≤index+1≤n,nums[index] 可以用于表示整数 index+1 是否出现两次。

遍历数组 nums。对于元素 num,其对应的下标为 index=∣num∣−1,根据 nums[index] 的正负性执行如下操作:

  • 如果 nums[index]>0,则将 nums[index] 的值更新为其相反数;

  • 如果 nums[index]<0,则 ∣nums∣=index+1 是出现两次的整数,将其添加到结果中。

上述做法的原理如下:

  1. 初始时数组 nums 中的整数都是正数,表示尚未被访问;

  2. 当一个整数被访问时,如果该整数对应的下标处的元素是正数,则该整数尚未被访问,因此将该整数对应的下标处的元素改成其相反数,相反数是负数,表示被访问了一次;

  3. 当一个整数被访问时,如果该整数对应的下标处的元素是负数,则该整数已经被访问,因此该整数被第二次访问,即该整数是出现两次的整数。

需要注意的是,遍历数组 nums 的过程中,遍历到的元素 num 可能已经被改成负数,因此在计算下标 index 时需要对 num 取绝对值然后减 1。

class Solution {
  public:
      vector<int> findDuplicates(vector<int>& nums) {
          vector<int> res;
          int n = nums.size();
          for (int nums : nums){
              int index = abs(nums) - 1;
              if(nums[index] > 0)  nums[index] = -nums[index];

              else  res.push_back(index + 1);
          }
          return res;
      }
  };
posted @ 2025-04-10 20:07  七龙猪  阅读(1)  评论(0)    收藏  举报
-->