11.13链表补充

11/13

每日一题:3261. 统计满足 K 约束的子字符串数量 II

思路:双指针+前缀和优化+二分查找

首先自己用昨天写的函数套上一个for循环,每次substr截取子字符串然后存到数组里。

时间复杂度是o(n^2)超时了,因此需要优化。

//超时代码
class Solution {
private:
    long long func(string s, int k) {
        long long ans = 0, left = 0, cnt[2]{};
        for (int i = 0; i < s.length(); i++) {
            cnt[s[i] & 1]++;
            while (cnt[0] > k && cnt[1] > k) {
                cnt[s[left] & 1]--;
                left++;
            }
            ans += i - left + 1;
        }
        return ans;
    }
public:
    vector<long long> countKConstraintSubstrings(string s, int k, vector<vector<int>>& queries) {
        vector<long long> ans;
        for (int i = 0; i < queries.size(); i++) {
          int l = queries[i][0];
          int r = queries[i][1];
          string t = s.substr(l , r - l + 1);
          ans.push_back(func(t , k));
          cout << ans[i];
        }
        return ans;
    }
};

image-20241113154603132

首先在滑动窗口的基础上,每次记录下右端点r满足条件的最小左端点l,即left[r] = l,一旦左端点<l就不满足。

然后用前缀和数组记下每个右端点的合法字符串数量sum[r]。

对于每个询问[l , r]可以分两种情况讨论:

  • 如果有left[r] <= l ,那么[l , r]里面的所有字符串均满足,字符串长度为r - l + 1,可以用等差数列直接计算数量= 1 + 2 + 3 + ····r - l + 1 = (r - l + 2) *(r - l + 1) / 2
  • 如果left[r] > l,那么有部分合法有部分不合法,设分界点为k(left(k)>l && left(k - 1) <= l)由于left数组是有序的,那么k左边的可以用等差数列求和,右边的=num(k+1)+num(k+2)+···num(r)。而这部分和可以用前缀和计算=sum[r] - sum[k]。
  • 所以关键点在于找分界点k,可以用二分查找。

二分找k:为第一个满足left(k) > l 的点,用lower_bound函数实现

lower_bound函数

从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

关于本题中lower_bound函数的用法解释:

找第一个 >= l的数K,如果区间内所有数<l,结果是k = r + 1

 int k = lower_bound(left.begin() + l , left.begin() + r + 1 , l) - left.begin();

起始指针位置为left.begin()向右偏移l个单位,因为left[i], i<l时必定有left[i] <l,意思是区间内均满足。下面要写到结尾位置+1。

image-20241113161445923

class Solution {
public:
    vector<long long> countKConstraintSubstrings(string s, int k, vector<vector<int>>& queries) {
       int n = s.size();
       vector<int> left(n);//记录每个位置满足条件的最小l下标
       vector<long long> sum(n + 1);//前缀和数组
       int cnt[2]{} , l = 0;
       for (int r = 0; r < n; r++) {
          cnt[s[r] & 1] ++;
          while(cnt[0] > k && cnt[1] > k){
            cnt[s[l ++] & 1] --;
          }
          left[r] = l;
          sum[r + 1] = sum[r] + r - l + 1; // 计算 r-left[r]+1 的前缀和
       }
        vector<long long> ans(queries.size());
      
        for (int i = 0; i < queries.size(); i++) {
           int l = queries[i][0] , r = queries[i][1];
     //也可写作:
     // for (auto& query : queries) {
      //int l = query[0], r = query[1];
          
           //找第一个 >= l的数K,如果区间内所有数<l,结果是k = r + 1
           int k = lower_bound(left.begin() + l , left.begin() + r + 1 , l) - left.begin();
           //分成l~k - 1 和 k ~ r两部分,由于前缀和数组从1开始,注意要下标+1,即计算 k+1~r+1 内的总和
           ans[i] = sum[r + 1] - sum[k] + (long long)(k - l + 1) * (k - l) / 2;
        }
        return ans;
        }
};
  • 时间复杂度:O(n+qlogn),其中 n 是 nums 的长度,q 是 queries 的长度。注意 l 只会增加不会减少,所以二重循环的时间复杂度为 O(n)。
  • 空间复杂度:O(n)。返回值不计入。

19. 删除链表的倒数第 N 个结点

思路:双指针

经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

/**
 * 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* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* fast = dummyHead;
        ListNode* slow = dummyHead;
        while(n --) fast = fast->next;
        while(fast->next != NULL){
          fast = fast->next;
          slow = slow->next;
        } 
        //此时slow的下一个节点是待删除的节点
        ListNode* tmp = slow->next;
        slow->next = slow->next->next;
        delete(tmp);
        tmp = nullptr;
       
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }
};

面试题 02.07. 链表相交

思路:双指针

如果有相交,相交节点以及后续部分一定是位于链表最后部分的。不可能出现中间一段相等而后续不相等的情况。

  1. 首先计算链表A、B的长度lenA,lenB,假设lenA >lenB(如果不是则交换)。gap = lenA - lenB
  2. 定义curA = headA , curB = headB,把curA后移gap步保证AB结尾重合。
  3. 然后curAB移动判断是否相等。

面试题02.07.链表相交_2

半天AC不了,原来是等于号写成赋值了。。。

if(curA == curB) return curA;
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* curA = headA;
        ListNode* curB = headB;
        int lenA = 0 , lenB = 0;
        while(curA != NULL){
          curA = curA->next;
          lenA ++;
        }
        while(curB != NULL){
          curB = curB->next;
          lenB ++;
        }
        if(lenA < lenB){
          swap(headA , headB);
          swap(lenA , lenB);
        }
        curA = headA;
        curB = headB;
        int gap = lenA - lenB;
        while(gap --) curA = curA->next;
        while(curA != NULL){
          if(curA == curB) return curA;
          curA = curA->next;
          curB = curB->next;
        }
        return NULL;
    }
};
  • 时间复杂度:O(n + m)
  • 空间复杂度:O(1)

M:142. 环形链表 II - 力扣(LeetCode)

思路:快慢指针

  1. 判断链表是否有环

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

2.如果有环,如何找到这个环的入口

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到fast指针与slow指针相遇节点 节点数为y。 从相遇节点再到环形入口节点节点数为 z。 如图所示:

img

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走的圈数。 (y+z)为 一圈内节点的个数A。

fast指针走过的节点数 = slow指针走过的节点数 * 2:即(x + y) * 2 = x + y + n (y + z)

消掉一个(x+y)后: x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

142.环形链表II(求入口)

  • 当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2

index1index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点

  • n大于1,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候效果是一样的,一样可以通过这个方法找到环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
      //这里while里面的条件是因为下面要用到:fast->next,因此保证fast非空;fast->next->next,因此保证fast->next非空。
        while(fast != NULL && fast->next != NULL){
          fast = fast->next->next;
          slow = slow->next;
          if(fast == slow) {
            ListNode* cur1 = head;
            int cnt = 0;
            while(cur1 != slow){
               cur1 = cur1->next;
              slow = slow->next;
          }
          return cur1;
        }
        }
        return NULL;
    }
};

234. 回文链表

思路:有数组模拟法和双指针法

  1. 数组模拟

最直接的想法,就是把链表装成数组,然后再判断是否回文。

 // 比较数组回文
        for (int i = 0, j = vec.size() - 1; i < j; i++, j--) {
            if (vec[i] != vec[j]) return false;
        }
        return true;
    }
  1. 快慢指针法

定义fast每次走两步,slow每次走一步,快指针遇到终止位置时,慢指针就在链表中间位置(此时fast可能为NULL,但是没关系,我们只需要slow的位置)

如果链表长度为奇数,那slow恰好在中间位置,如{1 ,2,3 , 2 ,1}

如果链表长度为偶数,那应该在中点靠右的位置,如{1 , 2 , 2 , 1}

pre记下slow的前一个位置,以pre为界限将链表划分为两部分cur1、cur2,len(cur1)<=len(cur2)。然后用前面学过的链表翻转将cur2翻转,最后从头遍历cur1、cur2,判断是否相等即可。

img

/**
 * 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* reverse(ListNode* head){
      if(head == NULL || head->next == NULL) return head;
      ListNode* last = reverse(head->next);
      head->next->next = head;
      head->next = NULL;
      return last;
    }
    bool isPalindrome(ListNode* head) {
      if(head == NULL || head->next == NULL)  return true;
        ListNode* fast = head;
        ListNode* slow = head;
        ListNode* pre = head;
        while(fast  && fast->next ){
          pre = slow;
          fast = fast->next->next;
          slow = slow->next;
        }
        //此时pre指向中点,将pre右边全部翻转
        pre->next = NULL;//分割链表

        ListNode* last = reverse(slow);//last为新表2表头
         ListNode* cur = head;

        while(cur){//cur长度小,以cur非空为终止条件
          if(cur -> val != last -> val)  return false;
          cur = cur->next;
          last = last->next;
        }
        return true;
    }
};

143.重排链表

思路:

  1. 数组模拟

先把所有节点存在数组vec里,然后通过cnt判断每次把vec[j]加入还是vec[i]加入。

 			cur = head;
        int i = 1;
        int j = vec.size() - 1;  // i j为之前前后的双指针
        int count = 0; // 计数,偶数去后面,奇数取前面
        while (i <= j) {
            if (count % 2 == 0) {
                cur->next = vec[j];
                j--;
            } else {
                cur->next = vec[i];
                i++;
            }
            cur = cur->next;
            count++;
        }
  1. 双向队列模拟

把链表放进双向队列,然后通过双向队列一前一后弹出数据,来构造新的链表。这种方法比操作数组容易一些,不用双指针模拟一前一后了。

		 ListNode* node;
     while(que.size()){
      if(cnt % 2 == 0){
        node = que.back();
        que.pop_back();
      }
      else{
        node = que.front();
        que.pop_front();
      }
      cnt ++;
      cur->next = node;//把node接到cur后边
      cur = cur->next;
     }
  1. 将链表分割成两个链表,然后把第二个链表反转,之后在通过两个链表拼接成新的链表。

    如图:

    img

    这种方法,比较难,平均切割链表,看上去很简单,真正代码写的时候有很多细节,同时两个链表最后拼装整一个新的链表也有一些细节需要注意!

    本题因为cur1所在的链表1本身带有head节点,所以应该比链表2长一个,因此分割时使slow为分割点,链表2为slow->next后的部分。

    		if (!head)  return;
          ListNode* fast = head;
          ListNode* slow = head;
          while(fast && fast->next && fast->next->next){
            fast = fast->next->next;
            slow = slow->next;
          }
          //以slow为分割点,slow停在中点或中点左侧
          ListNode* cur2 = reverse(slow->next);//cur2指向最后一个节点,slow->next为cur2的尾节点。
          slow->next = NULL;
          ListNode* cur = head;
          ListNode* cur1 = cur->next;	
    

vec数组:

class Solution {
public:
    void reorderList(ListNode* head){
      vector<ListNode*> vec;
      ListNode* cur = head;
      if(!cur) return;
      while(cur){
        vec.push_back(cur);
        cur = cur->next;
      }
      cur = head;
      int i = 1;
      int j = vec.size() - 1;
      int cnt = 0;
      while(i <= j){
        if(cnt % 2 == 0){
          cur->next = vec[j];
          j --;
        }
        else {
          cur->next = vec[i];
          i ++;
        }
        cur = cur->next;
        cnt ++;
      }
      cur->next = NULL;
    }  
};

deque:

class Solution {
public:
    void reorderList(ListNode* head){
     deque<ListNode*> que;
     ListNode* cur = head;
     if(!cur)  return;
     //从第二个节点开始存入双向队列
     while(cur->next){
      que.push_back(cur->next);
      cur = cur->next;
     }
     cur = head;
     int cnt = 0;
     ListNode* node;
     while(que.size()){
      if(cnt % 2 == 0){
        node = que.back();
        que.pop_back();
      }
      else{
        node = que.front();
        que.pop_front();
      }
      cnt ++;
      cur->next = node;//把node接到cur后边
      cur = cur->next;
     }
     cur->next = NULL;
    }  
};

分割翻转法:

class Solution {
private:
  ListNode* reverse(ListNode* head){
    ListNode* tmp;
    ListNode* cur = head;
    ListNode* pre = NULL;
    while(cur){
      tmp = cur->next;
      cur->next = pre;
      pre = cur;
      cur = tmp;
    }
    return pre;
  }

public:
    void reorderList(ListNode* head){
      if (!head)  return;
      ListNode* fast = head;
      ListNode* slow = head;
      while(fast && fast->next && fast->next->next){
        fast = fast->next->next;
        slow = slow->next;
      }
      //以slow为分割点
      ListNode* cur2 = reverse(slow->next);//cur2指向最后一个节点
      slow->next = NULL;
      ListNode* cur = head;
      ListNode* cur1 = cur->next;

      int cnt = 0;
      while(cur1 && cur2){
        if(cnt % 2 ==0){
          cur->next = cur2;
          cur2 = cur2->next;
        }
        else{
          cur->next = cur1;
          cur1 = cur1->next;
        }
        cnt++;
        cur = cur->next;
      }
      if(cur2) cur->next = cur2;
      if(cur1) cur->next = cur1;
    }  
};
posted @ 2024-11-13 22:20  七龙猪  阅读(2)  评论(0)    收藏  举报
-->