2.链表相关

1.链表基础

1.1数组和链表的优缺点

数组缺点:

①动态数组的长度也有可能超过实际存储数组元素所需的长度

②在实时操作系统中对操作的摊销边界是不可接受的

③一个数组内部执行插入和删除操作的代价太高

链表缺点:

没有办法随机存取

1.2 链表定义

链表是一种通过指针串联在一起的线性结构,每一个节点又有两部分组成,一个是数据域一个是指针域,最后一个指针域指向 Null

1.3 链表的类型

1.3.1 单链表

定义:由多个节点集合共同构成一个线性序列,每个节点存储一个对象的引用,这个引用指向序列中的一个元素,即存储指向列表中的下一个节点

缺点:没有办法有效的删除节点,因为不知道删除节点的前序节点

1.3.2 双链表

定义:定义一个链表,每个节点都维护了指向其先驱节点以及后继节点的引用。每个节点都有两个指针域,一个指向上一个节点,一个指向下一个节点。两个指针分别是 :next ,prev

双链表的优点:相比于单链表双链表双链表更容易删除节点,因为给一个单链表某个节点的引用很难找到其前面的节点。所以双链表对节点删除十分友好

1.3.3 循环链表

循环链表的首尾是相连的

1.4 链表的存储方式

链表在内存当中不是连续分布的,而是散乱的分布在内存中的某些地址上

1.5 链表的定义

如何定义一个链表的 Node:

struct ListNoe{
  int val; // 节点上存储的元素
  ListNode *next; // 指向下一个节点的指针
  ListNode(int x):val(x),next(nullptr){} // 节点的构造函数
};

LeetCode 官方答案:

/**
 * 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) {}
 * };
 */

1.6 链表代码易错点

1.当有删除操作的时候,如果这时候 head 是真正的节点则需要添加一个虚头节点 dummy ,否则 head 实 head 节点需要单独处理

2.很多时候需要 while 循环,但是 while 循环的条件要看我们方法体中要实现什么内容,是否要对 cur 进行处理,如果要对 cur 进行处理判断则 cur!= None ,主要看方法体对谁记性处理在 while 循环的条件就要自主定义

3.如果 while 循环条件是 cur.next !=None,如果循环结束,这时候 cur 指向的是 tail_node

1.7 得到链表倒数第 N 个节点题

要想得到倒数第 k 个节点就需要有两个指针,当 fast 指针指向 nullptr 也就是最后一个节点的时候 ,slow 指向的位置就是倒数 k 个节点的位置

如下图所示:

S1:假设想要删除倒数第 2 个 node 4 ,我们必须要让 slow 走到 node 3 的位置,也就是 fast 走到 node nullptr 时 ,slow 要走到 node 3

S2:先移动 fast 指针,在 k 步之后移动 slow 指针,这个主要是通过画图来看

找到 fast 在 nullptr 时,slow 所在的位置,就能知道我们需要让 fast 先走几步然后再让 slow 去走了,这个就是靠数

image-20221006114451057

2.LeetCode 相关题目

2.1_203移除链表元素

2.1.1 算法描述

1.定义一个虚头节点

这里需要添加一个 dummy ,因为有可能需要删除头结点

image-20221004102837487

在删除的时候 fast 要先走 k+1 步,然后 slow 再走

最后 fast 走到的是 NULL 就不能走了,在最后一个节点还是可以走的

关键是:先判断 fast 先从哪里走

2.1.2 代码实现

ListNode* removeNthFromEnd(ListNode* head, int n) {
  // write code here
  ListNode* dummy = new ListNode(0);
  dummy->next = head;
  ListNode* fast = dummy;
  ListNode* slow = dummy;
  // 先让 fast 走 n 步
  for(int i = 0;i<n;i++){
    fast = fast->next;
  }
  fast = fast->next;
  while(fast){
    fast = fast->next;
    slow = slow->next;
  }
  slow->next = slow->next->next;
  return dummy->next;
}
};

2.1.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(1)

2.2面试:_707设计链表(import)

2.2.1 算法描述

这里使用虚头节点的方式处理问题,分别在单链表和双链表中实现一下几个函数

其中 dummy 节点也被成为哨兵节点

2.2.2 代码实现

1.创建链表的节点元素

// 构造一个链表节点
struct ListNode{
  int val;
  ListNode *next;
  ListNode(int x):val(x),next(nullptr){};
};

2.初始化一个空的链表

初始化空的链表里面只需要有一个 dummy 节点和链表的大小

int _size;
ListNode *_dummy;
MyLinkedList() {
  // 定义一个虚头结点和一个 size 即可
  _dummy = new ListNode(0);
  _size = 0;
}

3.得到索引为 index node 的值

int get(int index) {
  // 判断范围
  if(index>_size-1||index<0) return -1;
  ListNode *cur = _dummy->next; // 这里得到的就是 cur 
  while(index--){
    cur = cur->next;
  }
  return cur->val;
}

4.在头部增加节点

void addAtHead(int val) {
  ListNode *newNode = new ListNode(val);
  newNode->next = _dummy->next;
  _dummy->next = newNode;
  _size++;
}

5.在尾部增加节点

void addAtTail(int val) {
  ListNode *cur = _dummy;
  ListNode *newNode = new ListNode(val);
  while(cur->next!=NULL){ // 指向最后一个节点后跳出循环
    cur = cur->next;
  }
  cur->next = newNode;
  newNode->next = nullptr;
  _size++;
}

6.在 index 位置增加节点

void addAtIndex(int index, int val) {
  if(index>_size||index<0) return;
  ListNode *newNode = new ListNode(val);
  ListNode *pre = _dummy; // 要指向插入 index 的前一个节点
  while(index--){
    pre = pre->next;
  }
  newNode->next = pre->next;
  pre->next = newNode;
  _size++;
}

7.删除 index 节点

void deleteAtIndex(int index) {
  if(index>_size-1||index<0) return;
  ListNode *pre = _dummy; // 要指向插入 index 的前一个节点
  while(index--){
    pre = pre->next;
  }
  ListNode *tmp = pre->next;
  pre->next = pre->next->next;
  delete tmp;
  _size--;        
}

8.遍历链表

// 打印链表
void printLinkedList() {
  LinkedNode* cur = _dummyHead;
  while (cur->next != nullptr) {
    cout << cur->next->val << " ";
    cur = cur->next;
  }
  cout << endl;
}

需要注意的点:

1.在删除时要将 cur 指针定位到删除 index 的前一个位置,所以在最开始 cur 就要指向 dummy 而不是 head

2.3_206翻转链表

2.3.1.算法描述

这里有三个指针比较重要:pre ,cur ,next 。

image-20220111125304210

关键代码为:

cur->next = pre;
cur = pre;
cur = nextcur

在这里插入图片描述

2.3.2代码实现

1.从前向后递归

class Solution {
  public:
  // 从前向后进行递归
  ListNode* reverse(ListNode* pre,ListNode* cur){
    if(cur==NULL) return pre;
    ListNode *tmp = cur->next; // 得到下一个要处理的节点 node5
    // 假设当前处理 node 4,pre = 3 cur = 4
    cur->next = pre; // 将 3 接在 4 的后面,pre 之前的都已经处理好了
    return reverse(cur,tmp);

  }
  ListNode* reverseList(ListNode* head) {
    return reverse(NULL,head);
  }
};

需要注意:

1.递归函数的起始元素是 null 和 head ,这样就不用再对一开始的 head 单独进行 null 的操作

2.当 cur 为 null 时返回的是 pre !!!,不是返回 null

2.迭代法

(1)C++

class Solution {
  public:
  ListNode* reverseList(ListNode* head) {
    ListNode* pre = head;
    ListNode* cur = head->next; // 要改变指针的就是 cur 
    while(cur!=nullptr){
      ListNode* tmp = cur->next;
      cur->next = pre;
      pre = cur;
      cur = tmp;
    }
    head->next = nullptr;
    return pre;

  }
};

(2)Python

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(head==nullptr) return nullptr;
        ListNode *cur = head->next;
        ListNode *pre = head;
        head->next = NULL;
        while(cur!=NULL){
            ListNode *tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
    
};

2.3.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(1)

2.4_24两两交换链表中的节点

2.4.1 算法描述

1.为什么需要添加 dummy

这里不是删除但是添加了 dummy 。

image-20211128150443022

现在的 head 是 node1 ,在对这个 node 1 交换时有一步是需要将 head 赋值给 node2 ,因为 head 在交换之后产生了改变。但是在交换 node3 和 node4 时就不用再将 head 从新赋值给 node3 ,所以这就产生了 head 节点需要单独处理的情况,对于 head 节点需要单独处理的节点要使用 dummy 节点,这样对 head 节点的处理可以一起在循环当中执行

2.本题节点之间的交换顺序为:

image-20211128161747725

2.4.2 C++ 代码实现

1.迭代实现

class Solution {
  public:
  ListNode* swapPairs(ListNode* head) {
    ListNode *dummy = new ListNode(0);
    ListNode *pre = dummy;
    pre->next = head;
    while(pre->next!=nullptr&&pre->next->next!=nullptr){ // cur 不为空,并且 cur 可以交换
      ListNode *tmp1 = pre->next; //1
      ListNode *tmp2 = pre->next->next; // 2
      tmp1->next = tmp2->next; // 1->3
      pre->next = tmp2;
      tmp2->next = tmp1; // 2->1
      pre = tmp1;
    }
    return dummy->next;
  }
};

易错点:

①因为交换一次数据需要牵扯好几个节点,所以交换时可以将节点暂时赋值给一个中间变量如:tmp2 ,tmp1 之类的

②这里的 pre 必须要从 dummy 开始,如果跳过了 dummy 直接交换 node1 node2 那么链就断了

2.递归实现

这个递归实现是依据上面的方法实现的

class Solution {
  void reverse(ListNode *pre){
    // 先序遍历
    if(pre==nullptr||pre->next==nullptr||pre->next->next==nullptr) return ; // 没有交换的意义
    ListNode *tmp1 = pre->next;
    ListNode *tmp2 = pre->next->next;
    pre->next = tmp2;
    tmp1->next = tmp2->next;
    tmp2->next = tmp1;
    reverse(tmp1);
  }
  public:
  ListNode* swapPairs(ListNode* head) {
    ListNode *dummy = new ListNode(0);
    dummy->next = head;
    ListNode *pre = dummy;
    reverse(dummy);
    return dummy->next;
  }
};

2.4.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(1)

2.5_19删除链表的倒数第 N 个节点

2.5.1算法描述

暴力思想:

  • 使用一个指针从前向后遍历找到 node5 ,同时定义一个计数器 count ,数从 node1 走到 node5 需要多少步
  • 使用公式 count-n 找到 pre 指针
  • 再使用一个 while 循环将指针移动到 pre 的 index
  • 最后执行删除操作

快慢指针:

使用双指针(快慢指针)的思想可以减少 while 循环的次数。

设置一个 low ,这个 low 最终代表的是 pre 。low 在经过 n 个 next 之后就会走到最后一个节点

当 fast 走了 n+1 步之后这个 low 才会开始走

因为是删除,这里需要指定一个 dummy 节点

image-20211128170609428

2.5.2 C++ 代码实现

class Solution {
  public:
  ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode *dummy = new ListNode(0);
    dummy->next = head;
    // 设置两个指针 
    ListNode *fast = dummy;
    ListNode *low = dummy;
    int i = 0; // 从 0 开始
    while(fast->next!=NULL){
      fast = fast->next;
      if(i++>=n) low = low->next; // 保证 low 和 fast 永远相差 n 个 next 
    }
    // 删除 pre 后面的节点
    ListNode *cur = low->next;
    low->next = cur->next;
    delete(cur);
    return dummy->next;
  }
};

2.5.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(1)

2.6_面试题02.07链表相交

2.6.1 算法描述

先求出两个子串的长度,然后将两个子串进行对齐。对齐后同时走,如果指针相同就说明相交,如果到了最后还没相交就说明没有相交节点

image-20211128175839745

2.6.2 C++ 代码实现

class Solution {
  public:
  ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
    // 计算两个链表的长度
    ListNode *a = headA;
    ListNode *b = headB;
    int len_a = 0;
    int len_b = 0;
    // 计算 A 的长度
    while(a!=nullptr){
      len_a++;
      a = a->next;
    }
    // 求取 B 的长度
    while(b!=nullptr){
      len_b++;
      b = b->next;
    }

    // 获得长串和子串然后遍历
    ListNode *curA = headA; // 指向长串
    ListNode *curB = headB; // 指向短串
    int cha = 0;
    if(len_a<=len_b){
      swap(len_a,len_b);
      swap(curA,curB);   
    }
    cha = len_a-len_b;
    // 现将 A 移动到相应位置
    while(cha--) curA = curA->next;
    while(curA!=nullptr){
      if(curA==curB) return curA;
      curA = curA->next;
      curB = curB->next;
    }
    return nullptr;        
  }
};

2.6.3 时空复杂度

时间复杂度:O(M+N)

空间复杂度:O(1)

2.6.4 扩展知识—交换两个变量的值

以前是再添加一个变量交换两个变量的值,在 C++ 中提供了函数 swap

swap(a,b); // 交换变量 a b 的值 	

2.7_14环形链表2

2.7.1 算法描述

1.链表是否有环

①存在环:

如果 fast 指针和 slow 指针相遇了,则代表存在环

他们相遇的地方肯定是在环内的某个位置

相遇时 fast 比 slow 多走了 n 圈

②不存在环

fast 最后走向了 None

2.如果有环,环的入口 在哪

①现在假设存在环:

x = 从 head 节点到环的入环口的 step 数

y = 入环口到 fast 和 low 相遇的地方的 step 数

z = 相遇节点剩余部分再到入环口为 z

img

②相遇时两个节点走的步数

slow : x+y

fast:x+n(z+y)+y

fast = 2 slow

相关数学公式推导:

等式:2*(x+y)=x+y+n(y+z)

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

求得x 的值:x = n (y + z) - y

在从n(y+z)中提出一个 (y+z)来,整理公式之后:x = (n - 1) (y + z) + z

这里的 n 是大于等于1 的,因为 fast 要至少走完 1 圈才会和 slow 相遇

当 n=1 时: x=z

这就意味着,从 head 出发一个指针,从相遇节点也出发一个指针,两个节点同时走相同的步数, 那么当这两个指针相遇的时候就是 环形入口的节点

当 n>1 时这个等式仍然成立,因为我们所求的是位移,不是路程,所以上面等式和 fast 多绕了几圈是没有关系,不妨碍成立的

③代码设计思路

在存在闭环的情况下,在找到相遇的节点之后,分别定义一个指针从 head 出发,然后再定义一个指针从相遇节点出发,走相同的步数后,如果两个指针相遇则相遇的节点就是环的入口

易错点:

这里的 x y 必须在非入口的地方相遇,如果是在入口初相遇则永远追不上,slow 如果是在后面进行更新的那么就会在入口处相遇

2.7.2 C++ 代码实现

class Solution {
  public:
  ListNode *detectCycle(ListNode *head) {
    // 创建快慢指针
    ListNode *fast = head;
    ListNode *slow = head;
    while(fast!=nullptr&&fast->next!=nullptr){
      slow = slow->next; // 只有将 slow 的移动放在最前面才不会超时
      fast = fast->next->next;
      // 快慢指针相遇
      if(fast==slow){
        ListNode *q = fast;
        ListNode *p = head;
        while(q!=p){
          q = q->next;
          p = p->next;
        }
        return p; // 返回相遇的节点
      }
    }
    return NULL;        
  }
};

易错点:

有可能出现没有环的情况,所以 while 循环的条件不能是 fast == slow 才停止,如果 fast 或者 fast->next->next 就停止

2.7.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(1)

3.其他题目

3.1_21合并两个有序的链表

链表+递归

LeetCode题目链接

3.1.1 算法描述

链表插入时要一个一个节点的插入

需要定义一个虚头 dummy 节点,这里和树的合并不一样,只需要定义一个 dummy 节点,而树的合并是直接进行合并的

3.1.2 C++ 代码实现

1.迭代方法

class Solution {
  public:
  ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
    // 定义 dummy 节点
    ListNode *dummy = new ListNode(-1);
    ListNode *pre = dummy;
    while(list1!=nullptr&&list2!=nullptr){
      if(list1->val<list2->val){
        pre->next = list1;
        list1 = list1->next;
      }else{
        pre->next = list2;
        list2 = list2->next;
      }
      pre = pre->next;
    }
    // 将剩余的链表拼接上
    pre->next = list1 == nullptr ?list2:list1;
    return dummy->next;

  }
};

易错点:

①这是链表题目,在进行合并时可以新定义 dummy 节点进行拼接会更加容易,而不是将 list2 拼接到 list1 上

2.递归

class Solution {
  public:
  void merge(ListNode* pre,ListNode* pHead1,ListNode* pHead2){
    if(pHead1==nullptr&&pHead2!=nullptr){
      pre->next = pHead2 ;
      return ;
    } 
    else if(pHead1!=nullptr&&pHead2==nullptr){
      pre->next = pHead1 ;
      return;
    } else if(pHead1==nullptr&&pHead2==nullptr) return;
    if(pHead1->val<pHead2->val){
      pre->next = pHead1;
      merge(pre->next,pHead1->next,pHead2);
    }else{
      pre->next = pHead2;
      merge(pre->next,pHead1,pHead2->next);
    }
  }
  ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {

    ListNode* pre = new ListNode(0);
    merge(pre,pHead1,pHead2);
    return pre->next;

  }
};

3.1.3 时空复杂度

时间复杂度:O(n+m)

空间复杂度:O(1)

3.2_23合并K个升序链表

链表

3.2.1 算法描述

1.暴力

可以借鉴两个有序链表插入的情况,将链表进行两两插入

List1 和 list2 进行插入,返回产生的新链表 newList ,然后新链表再和 list3 进行两两插入

为了防止 lists 中只有一个元素,那这样就不会进行拼接的 for 循环,所以这里创建一个 dummy list ,保证至少有两个 list 可以进行拼接

2.归并

image-20220318130414204

这里我们可以将一整个排好序的子 list 看做是一个整体,然后使用归并的方法找到当前要进行归并的 index = left 和 index =right 的 list 。进行 list 之间的两两合并,合并之后会返回一个完整的 list ,然后将左右两边的 list 再进行 merge。

这里和数组的归并不一样,这里不需要额外的开辟空间,所以 merge 方法需要返回值

image-20220318124045175

3.2.2 代码实现

1.暴力

class Solution {
  ListNode* mergeKLists(ListNode* list1, ListNode* list2){ // 链表的两两合并
    // 定义 dummy 节点
    ListNode *dummy = new ListNode(-1);
    ListNode *pre = dummy;
    while(list1!=nullptr&&list2!=nullptr){
      if(list1->val<list2->val){
        pre->next = list1;
        list1 = list1->next;
      }else{
        pre->next = list2;
        list2 = list2->next;
      }
      pre = pre->next;
    }
    // 将剩余的链表拼接上
    pre->next = list1 == nullptr ?list2:list1;
    return dummy->next; // 返回新链表的头结点
  }
  public:
  ListNode* mergeKLists(vector<ListNode*>& lists) {
    ListNode* newList = nullptr;
    for(int i =0;i<lists.size();i++){
      newList = mergeKLists(newList,lists[i]);
    }
    return newList;
  }
};
  1. 归并
class Solution {
public:
    ListNode* merge(ListNode* list1,ListNode* list2){
        ListNode* dummy = new ListNode();
        ListNode* pre = dummy;
        while(list1!=nullptr&&list2!=nullptr){
            // 判断大小
            if(list1->val<list2->val){
                pre->next = list1;
                list1 = list1->next;
            }else{
                pre->next = list2;
                list2 = list2->next;
            }
            pre = pre->next;
        }
        // 肯定有一个为 null
        if(list1==nullptr) pre->next = list2;
        else pre->next = list1;   
        return dummy->next;
    }
    // 用递归的方式找到需要合并的链表
     ListNode* split(vector<ListNode*>& lists ,int left,int right){
        if(left==right) return lists[left];
        if(left>right) return nullptr;
        int mid = (right-left)/2+left;
        ListNode* left_res = split(lists,left,mid);
        ListNode* right_res = split(lists,mid+1,right);
        ListNode* res = merge(left_res,right_res);
        return res;
    }
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        ListNode* res = split(lists,0,lists.size()-1);
        return res;;

        

    }
};

3.3_138复制带随机指针的链表

参考资料

3.3.1 算法描述

1.暴力搜索和连接

如果使用暴力解法的话

S1:遍历一遍 old

遍历 old 链表的同时创建 node 然后将 node 进行连接,这里的连接只是对于 next 进行连接

S2:再遍历一遍 node

将 random 进行连接。在连接 random 时我们还要遍历一遍 link list ,因为 linklist 的搜索时间复杂度是 O(N),而且还要排除所搜索到的 node 有重复的问题

2.Hashmap

image-20220206125453772

S1:遍历 old_node

遍历 old_node 的同时创建 new_node 的节点,使用 map 形成 [旧 node ,新 node] 的映射

S2:进行 random 和 next 的连接

mp[cur]->next = mp[cur->next];
mp[cur->random] = mp[cur->random];
image-20220206205458003

3.3.2 代码实现

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;

    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
  public:
  Node* copyRandomList(Node* head) {
    unordered_map<Node*,Node*> mp;
    // 遍历生成 old->new
    Node* cur = head;
    while(cur!=NULL){
      Node* node = new Node(cur->val);
      mp[cur] = node;
      cur = cur->next;
    }
    cur = head;
    // 形成 next 和 random 的映射
    while(cur!=NULL){
      mp[cur]->next = mp[cur->next];
      mp[cur]->random = mp[cur->random];
      cur = cur->next;
    }
    return mp[head];

  }
};

3.3.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(N)

3.4_146LRU缓存

LeetCode 题目链接(评论区)

3.4.1 算法基本描述

对于这个题来说存放每一个 item 是需要基于一个容器的,这个容器需要满足能够进行随机插入删除和随机访问。众所周知这样同时满足这两样的容器是不存在的,需要使用两种容器结合的方式。Hash 随机访问比较快,链表插入删除比较快,所以将这二者结合

为什么使用 hash :很明显根据题目要就,要进行 key - val 的映射

为什么使用双向链表:使得删除时的时间复杂度为 O(1)

为什么 list 的 node 是 pair 类型,因为在找到最开头或者最结尾的 node 后需要通过 list node 的 key 值找到对应的 hash 。所以 node 中既要保存 key 也要保存 value

image-20220208121018417

下面的操作不论是 get 还是 put ,每一个操作过后都要改变 node 之间的顺序,将刚才访问的节点移动到链表的头部

下面是插入和删除的移动规则:

每访问一个 node 都要将这个 node 移动到开头的位置

// key 映射到 Node(key, val)
HashMap<Integer, Node> map;
// Node(k1, v1) <-> Node(k2, v2)...
DoubleList cache;

int get(int key) {
  if (key 不存在) {
    return -1;
  } else {        
    将数据 (key, val) 提到开头;
      return val;
  }
}

void put(int key, int val) {
  Node x = new Node(key, val);
  if (key 已存在) {
    把旧的数据删除;
      将新节点 x 插入到开头;
  } else {
    if (cache 已满) {
      删除链表的最后一个数据腾位置;
        删除 map 中映射到该数据的键;
    } 
    将新节点 x 插入到开头;
      map 中新建 key 对新节点 x 的映射;
  }
}

3.4.2_代码实现

class LRUCache {
  public:
  // 全局变量
  int cap;
  list<pair<int,int>> cache;
  unordered_map<int,list<pair<int,int>>::iterator> map; // value 存放的是 list node 的指针
  LRUCache(int capacity):cap(capacity) {

  }

  int get(int key) {
    if(map.find(key)==map.end()) return -1; // 不存在
    // node 存在:将其移动到最开始的位置
    auto key_value = *map[key]; // 得到该 node ,是一个 pair 
    cache.erase(map[key]); // 将其原先的 node 进行删除
    cache.push_front(key_value); // 将该节点插入到 cache 的开头
    map[key] = cache.begin(); // 在 hash 进行 key-node 的重新映射
    return key_value.second; // 返回 value 值
  }

  void put(int key, int value) {
    // node 不存在
    if(map.find(key)==map.end()){
      if(cache.size()==cap){ // cap 满了:先删掉最后一个元素
        // 最后一个 node 删除,map + cache 删除
        map.erase(cache.back().first); 
        cache.pop_back();
      }
    }else{ // node 存在
      cache.erase(map[key]);
    }
    // 将 node 放入
    cache.push_front({key,value});
    map[key] = cache.begin();

  }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

3.4_460_ LFU 缓存

https://leetcode.cn/problems/lfu-cache/solution/ha-xi-biao-shuang-xiang-lian-biao-c-by-l-e5e0/

3.4.1 算法描述

S1:定义两个 class

Node

一个 class 是 Node ,这个 Node 中包含 Node 的 key 还有 val 以及 freq 的信息,与此同时在后面我们需要将 Node 进行首尾相接,用于判断使用的频率所以 Node 还需要有 pre 和 next 节点

struct Node {
  int key;
  int val;
  int freq;
  Node* prev;
  Node* next;
  Node () : key(-1), val(-1), freq(0), prev(nullptr), next(nullptr) {}
  Node (int _k, int _v) : key(_k), val(_v), freq(1), prev(nullptr), next(nullptr) {}
};

FreqList

当一个 Node 使用相同的 Freq 的时候,我们就需要按照最近最少使用的方法将节点进行删除,这一点和 LRU 很像,所以需要几个 List ,在 freq 相同的时候移除一些不太使用的节点

越靠近 head 的 Node 是最近刚刚使用的 Node ,靠近 tail 的 Node 是相等 freq 中最近都没有在使用的

为了方便移除节点我们给 List 增加了 head 和 tail 两个 dummy 节点

struct FreqList {
  int freq;
  Node* vhead;
  Node* vtail;
  FreqList (int _f) : freq(_f), vhead(new Node()), vtail(new Node()) {
    vhead->next = vtail;
    vtail->prev = vhead;
  }
};

S2:FreqList 定义相关操作函数

FreqList 是通过 freq 作为 key 值进行查找的

(1)FreqList 的增加节点操作

首先判断这个 freq 是否有出现在 freq_map 中,如果没有的话我们需要创建一个新的 FreqList,然后再将 Node 插入在 head 之后即可

void addHead (Node* t) {
  int freq = t->freq;
  if (freq_map.find(freq) == freq_map.end()) {
    freq_map[freq] = new FreqList(freq);
  }
  FreqList* l = freq_map[freq];
  t->next = l->vhead->next;
  l->vhead->next->prev = t;
  t->prev = l->vhead;
  l->vhead->next = t;
}

(2)FreqList 的删除操作

删除操作是将 min_freq 指向的 list 的 tail 节点的 Node 进行删除

void popTail () {
  Node* t = freq_map[min_freq]->vtail->prev;
  deleteNode(t);
  occ.erase(t->key);
}

下面是具体的删除操作

void deleteNode (Node* t) {
  t->prev->next = t->next;
  t->next->prev = t->prev;
}

S3:cache 的 get 和 post 操作

(1)相关成员属性

min_freq 指针:在删除的时候我们删除的是最小 freq 中,tail 之前的那个 node ,所以我们需要有一个指向 min_freq 的指针

这里需要两个 map ,一个用于保存 key-val 一个用于保存 freq-list

unordered_map<int, Node*> occ;
unordered_map<int, FreqList*> freq_map;
int sz;
int min_freq;

(2)get 操作

首先要先判断 key 值是否存在于 key_map 中(假设存在)

其次得到 Node ,将它在 freq_map 中删除,对它进行 freq++ 操作

只要是出现了删除节点就要对 min_freq 进行判断,判断 min_freq 是否向下一个 freq 进行移动

然后将它插入到 FreqList 的 head 后面

int get (int key) {
  int res = -1;
  if (occ.find(key) != occ.end()) {
    Node* t = occ[key];
    res = t->val;
    deleteNode(t);
    t->freq++;
    if (empty(freq_map[min_freq])) min_freq++;
    addHead(t);
  }
  return res;
}

(3) put 操作

这里的 put 和 lru 的 put 不一样的是,当我们 put 时如果 key 是存在于 list 中的我们只需要更改这个 Node 的 val 值,他的 freq 和在 FreqList 中的位置都不会改变,但是如何这个 Node 是不在 key_map 中的,这个 freq 就是 1

void put (int key, int value) {
  if (sz == 0) return;
  if (get(key) != -1) {
    occ[key]->val = value;
  }
  else {
    if (occ.size() == sz) {
      popTail();
    }
    Node* t = new Node(key, value);
    occ[key] = t;
    min_freq = 1;//新插入的 频率一定最少, 为1
    addHead(t);
  }
}

3.4.2 代码实现

struct Node {
  int key;
  int val;
  int freq;
  Node* prev;
  Node* next;
  Node () : key(-1), val(-1), freq(0), prev(nullptr), next(nullptr) {}
  Node (int _k, int _v) : key(_k), val(_v), freq(1), prev(nullptr), next(nullptr) {}
};
struct FreqList {
  int freq;
  Node* vhead;
  Node* vtail;

  FreqList (int _f) : freq(_f), vhead(new Node()), vtail(new Node()) {
    vhead->next = vtail;
    vtail->prev = vhead;
  }
};


class LFUCache {
  private:
  unordered_map<int, Node*> occ;
  unordered_map<int, FreqList*> freq_map;
  int sz;
  int min_freq;
  public:
  LFUCache (int capacity) : sz(capacity) {}

  bool empty(FreqList* l) {
    return l->vhead->next == l->vtail ? true : false;
  }

  void deleteNode (Node* t) {
    t->prev->next = t->next;
    t->next->prev = t->prev;
  }

  void addHead (Node* t) {
    int freq = t->freq;
    if (freq_map.find(freq) == freq_map.end()) {
      freq_map[freq] = new FreqList(freq);
    }
    FreqList* l = freq_map[freq];
    t->next = l->vhead->next;
    l->vhead->next->prev = t;
    t->prev = l->vhead;
    l->vhead->next = t;
  }

  void popTail () {
    Node* t = freq_map[min_freq]->vtail->prev;
    deleteNode(t);
    occ.erase(t->key);
  }

  int get (int key) {
    int res = -1;
    if (occ.find(key) != occ.end()) {
      Node* t = occ[key];
      res = t->val;
      deleteNode(t);
      t->freq++;
      if (empty(freq_map[min_freq])) min_freq++;
      addHead(t);
    }
    return res;
  }

  void put (int key, int value) {
    if (sz == 0) return;
    if (get(key) != -1) {
      occ[key]->val = value;
    }
    else {
      if (occ.size() == sz) {
        popTail();
      }
      Node* t = new Node(key, value);
      occ[key] = t;
      min_freq = 1;//新插入的 频率一定最少, 为1
      addHead(t);
    }
  }
};

LFU:

这里在 put Node 时,当 Node 出现的频率不相等,谁的频率小移除谁,当出现频率相同时,谁最近没有被访问则移除谁

LRU:谁最近没有访问则移除谁

3.5_25 K 个一组反转链表

3.5.1 算法描述

S1:反转链表函数设计

这里我们需要对链表分组,一组一组的进行反转,每次反转 k 个。根据反转函数的设计这里需要我们传入所要反转链表的 head 和反转的次数

为了将新反转的链表进行拼接我们需要得到新链表的头和新链表的尾

image-20220302171810172

S2:找到需要反转的 head

根据最基础的反转函数我们仅需要传入 head 节点就可以进行反转,这里还需要传入反转的个数

①首先要判断这个链表是否需要反转,那就使用for 循环数个数

②数完个数之后我们就找个了当前 group 的 tail 节点,以及下一次需要 reverse 的 next_judge ,以防断链

③反转链表

④将反转后的链表拼接,继续判断下一个链表

3.5.2 代码实现

class Solution {
  public:
  pair<ListNode*,ListNode*> reverseList(ListNode* head,int count) {
    ListNode* pre = head;
    ListNode* cur = head->next; // 要改变指针的就是 cur 
    while(cur!=nullptr&&count>1){
      ListNode* tmp = cur->next;
      cur->next = pre;
      pre = cur;
      cur = tmp;
      count--;
    }
    head->next = nullptr;
    return {pre,head}; // 返回头结点和尾结点
  }
  ListNode* reverseKGroup(ListNode* head, int k) {
    ListNode* dummy = new ListNode(0);
    dummy->next = head; 
    ListNode* pre = dummy;
    // group 与 group 之间的反转
    while(head){
      // 开始选取一个 group 中的 node 
      ListNode* tail = pre; // 去找该 group 的尾结点
      // 先判断数目是否达到 k 个
      for(int i = 0;i<k;i++){
        tail = tail->next;
        if(tail==nullptr) return dummy->next; // 该 group 不用反转 
      }
      ListNode* next_judge = tail->next; // 记录下一次判断的头结点
      pair<ListNode*,ListNode*> res = reverseList(head,k);
      // 将 group 重新拼接会原链表
      pre->next = res.first; 
      res.second->next = next_judge;
      pre = res.second;
      head = next_judge; // 接着判断下一个group 
    }
    return dummy->next;
  }
};

3.6_328奇偶链表

3.6.1 算法描述

本题可以使用先创建数组再将数组转换成链表的形式。但是那样时间和空间复杂度都是不允许的。

这里定义两个指针 odd 和 even ,在防止断链的情况下将 node 组合起来

易错点:

①防止断链问题

这里注意断链的问题,比如想要将 node 1 和 node2 断开,那么 node3 必须要提前保存,一旦 node1->next = node3 了,那么我们就不能再通过 node1->next->next 定位到我们下一次需要操作的 node 3 ,也就是 ①->②->③ ,所以,当操作完 node 1 后要及时将 node 3 保存到变量中

②while 循环的条件

while 结束循环的条件是 even ,因为 odd 和 even 是成对出现的,如果 even 为 null 或者 even->next 为空则不需要继续判断

image-20220320082149287

3.6.2 代码实现

class Solution {
  public:
  ListNode* oddEvenList(ListNode* head) {
    // 特殊情况
    if(head==nullptr) return head; // 没有节点
    if(head->next==nullptr) return head; // 没有偶数节点
    if(head->next->next==nullptr) return head; // 不用反转

    // 两个指针
    ListNode* odd = head;
    ListNode* even = head->next;
    ListNode* even_head = head->next;
    while(even!=nullptr&&even->next!=nullptr){
      odd->next = even->next;
      odd = odd->next;
      even->next = odd->next;
      even = even->next;
    }
    odd->next = even_head;
    return head;

  }
};

3.6.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(1)

3.7_82删除排序链表中的重复元素 II

参考资料

3.7.1 算法描述

1.递归实现

S1:特殊情况

有两种情况是不用删除的,没有 head 节点和 只有 head 节点一个元素

S2:我们需要判断 head 节点和 head->next 之前的关系

①要删

else{
  // 不断的删除节点
  ListNode* move = head->next;
  while(move&&head->val==move->val) move = move->next;
  return deleteDuplicates(move);
}

这时候有一个动态节点 move ,要一直移动 move 节点,直到 move 节点和 head 节点之间的值不相等,这样就直接返回 move 节点了,因为 head 节点是一个重复的节点了

②不用删

这个时候是不用删的,直接将 head 之后拼接 head->next ,也就是下一步递归的过程

if(head->val!=head->next->val){
  head->next = deleteDuplicates(head->next); // 开始判断 head 的下一个节点
}

3.7.2 代码实现

1.递归

class Solution {
public:
    ListNode* pre;
    void del(ListNode* cur){
        if(cur==nullptr||cur->next==nullptr) return;
        if(cur->val==cur->next->val){
            ListNode* move = cur->next;
            while(move&&move->val==cur->val) move = move->next;
            // 判断下面的节点
            pre->next = move;
            del(move);
        }else{
            pre = cur;
            del(cur->next);
        }
    }
    ListNode* deleteDuplicates(ListNode* head) {
        ListNode* dummy = new ListNode();
        dummy->next = head;
        pre = dummy;
        del(head);
        return dummy->next;

    }
};

2.非递归

class Solution {
  public:
  ListNode* deleteDuplicates(ListNode* head) {
    // 先判断几种不用删除的情况
    if(head==nullptr) return head;
    if(head->next==nullptr) return head;
    ListNode* dummy = new ListNode();
    dummy->next = head;
    ListNode* pre = dummy;
    ListNode* cur = head; // 判断 cur 是否需要删除
    while(cur!=nullptr&&cur->next!=nullptr){
      // 不需要删除
      if(cur->val!=cur->next->val){
        pre->next = cur;
        pre = cur;
        cur = cur->next;
      }else{ // 需要删除
        // 让 move 移动到需要拼接的元素那里
        ListNode* move = cur->next;
        while(move&&move->val==cur->val) move = move->next;
        pre->next = move;
        cur = move;
      }
    }
    return dummy->next;
  }
};

3.7.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(1)

3.8_143重排链表

3.8.1 算法描述

这里相当于将后面的节点向前面插入,而且是从后向前插,所以这里我们对后面的节点会做三个操作:

断开,反转,合并

image-20220628110532178

关于链表奇偶个数问题:

我们通过画图可能能发现,当链表长度为偶数时(如下图)我们反转的起始点为 slow->next->next ,但是当节点个数为奇数时我们反转的起点是 slow->next,但是这里不用判断节点个数时奇数还是偶数

image-20220628110754072

因为我们最后一步是两个链表的合并操作,也就是说如果在奇数的情况下多的哪一个 node 会在合并的时候直接合并到最后

所以不用分别是奇数还是偶数

3.8.2 代码实现

class Solution {
  public:
  void reorderList(ListNode* head) {
    if (head == nullptr) {
      return;
    }
    ListNode* mid = middleNode(head);
    ListNode* l1 = head;
    ListNode* l2 = mid->next;
    mid->next = nullptr;
    l2 = reverseList(l2);
    mergeList(l1, l2);
  }

  ListNode* middleNode(ListNode* head) {
    ListNode* slow = head;
    ListNode* fast = head;
    while (fast->next != nullptr && fast->next->next != nullptr) {
      slow = slow->next;
      fast = fast->next->next;
    }
    return slow;
  }

  ListNode* reverseList(ListNode* head) {
    ListNode* prev = nullptr;
    ListNode* curr = head;
    while (curr != nullptr) {
      ListNode* nextTemp = curr->next;
      curr->next = prev;
      prev = curr;
      curr = nextTemp;
    }
    return prev;
  }
  void mergeList(ListNode* l1, ListNode* l2) {
    ListNode* l1_tmp;
    ListNode* l2_tmp;
    while (l1 != nullptr && l2 != nullptr) {
      l1_tmp = l1->next;
      l2_tmp = l2->next;

      l1->next = l2;
      l1 = l1_tmp;

      l2->next = l1;
      l2 = l2_tmp;
    }
  }
};

3.8.3 时空复杂度

时间复杂度:O(N)

空间复杂度:O(1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值