C++ 数据结构与算法(四)(链表)

链表

1、类型及定义

1.1 单链表

(单)链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域,一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向 null(空指针的意思)。链接的入口节点也就是 head(头结点)。
代码随想录

1.2 双链表

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。
代码随想录 双链表

1.3 循环链表

链表首尾相连,可以用来解决约瑟夫环问题。
在这里插入图片描述

2、存储方式

数组是在内存中是连续分布的,但是链表在内存中不是连续分布的。

链表是通过指针域的指针链接在内存中各个节点。

所以链表中的节点在内存中 是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
代码随想录

3、链表的操作

3.1 结构体定义
// 单链表
struct ListNode {
    int val;  			// 节点上存储的元素
    ListNode *next;  	// 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数,初始化成员变量
    ListNode(int x) : val(x), next(nullptr) {}
 	ListNode(int x, ListNode *next) : val(x), next(next) {}
};

int main(void){
    ListNode * head = new ListNode(6);
    cout << head->val;
    return 0;
}
3.2 添加 和 删除 节点
  • 添加节点
    代码随想录 添加链表节点
  • 删除节点

只要将C节点的 next 指针 指向E节点就可以了。

D节点依然存留在内存里,所以在C++里最好是再手动释放这个D节点,释放这块内存。
代码随想录 删除链表节点
链表的添加和删除都是 O ( 1 ) O(1) O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度 O ( n ) O(n) O(n)

4、与数组的对比

代码随想录
数组是在内存中是连续分布的,但是链表在内存中不是连续分布的。

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

203. 移除链表元素 ●

代码随想录

1、迭代
  • 原链表直接操作

移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。

/**
 * 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* removeElements(ListNode* head, int val) {
        while (head != nullptr && head->val == val){        //  while 判断头结点
            ListNode * temp = head;                         // head 为要删除的节点,因此用临时节点保存
            head = head->next;                              // 将头结点向后移动一位
            delete temp;    								// 在节点覆盖后删除临时内存实现内存清理
        }
        
        ListNode * curr = head;                             // 新建节点 表当前遍历的节点curr
        while(curr != NULL && curr->next != NULL){          
            if (curr-> next -> val == val){                 // 检查当前节点的下一个节点的值
                ListNode * temp = curr->next;
                curr->next = curr->next->next;              // 替换、删除
                delete temp;
            }
            else curr = curr->next;                         // 遍历过程
        }
        return head;
    }
};
  • 设置虚拟头结点进行统一操作
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0); 	// 设置一个虚拟头结点
        dummyHead->next = head; 				// 将虚拟头结点指向head,这样方面后面做删除操作
        ListNode* cur = dummyHead;
        while (cur->next != NULL) {
            if(cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        head = dummyHead->next;				// 还原实际头结点
        delete dummyHead;					// 内存清理
        return head;
    }
};
  • 时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次。
  • 空间复杂度:O(1)。
2、递归

对于给定的链表,首先对除了头节点 head 以外的节点进行删除操作,然后判断 head 的节点值是否等于给定的 val。

如果 head 的节点值等于 val,则 head 需要被删除,因此删除操作后的头节点为 head.next;
如果 head 的节点值不等于 val,则 head 保留,因此删除操作后的头节点还是 head。上述过程是一个递归的过程。

递归的终止条件是 head 为空,此时直接返回 head。当 head 不为空时,递归地进行删除操作,然后判断 head 的节点值是否等于 val 并决定是否要删除 head。

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if (head == nullptr) {
            return head;
        }
        head->next = removeElements(head->next, val);		// 递归删除
        return head->val == val ? head->next : head;
    }
};
  • 时间复杂度:O(n),其中 n 是链表的长度。递归过程中需要遍历链表一次。

  • 空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用栈,最多不会超过 n 层。

707. 设计链表 ●●

1、单链表 增 删 查
class MyLinkedList {
public:
    struct ListNode{			// 自定义单链表节点
        int val;				
        ListNode *next;
        ListNode(int n) : val(n), next(nullptr) {}	// 构造函数
    };

private:
    ListNode *_dummyHead;		// 声明类成员(虚拟头结点)
    int _size;
    
public:
    MyLinkedList() {					// 类构造函数
        _dummyHead = new ListNode(0);	// 创建(虚拟)头结点
        _size = 0;						// 链表长度
    }
    
    int get(int index) {				// 取index位置中的元素值   index ~ [0, _size-1]
        int num = 0;			
        ListNode * curr = _dummyHead->next;
        while(curr != nullptr){
            if(num == index){
                return curr->val;
            }
            num++;
            curr = curr->next;
        }
        return -1;
    }
    
    void addAtHead(int val) {		// 头部添加
        ListNode * newHead = new ListNode(val);
        newHead->next = _dummyHead->next;
        _dummyHead->next = newHead;
        _size++;
    }
    
    void addAtTail(int val) {		// 尾部添加
        ListNode * curr = _dummyHead;
        while(curr->next != nullptr){
            curr = curr->next;
        }
        curr->next = new ListNode(val);
        _size++;
    }
    
    void addAtIndex(int index, int val) {		// 指定位置添加
        if(index < 0){
            addAtHead(val);
        }
        else if(index == _size){
            addAtTail(val);
        }
        else if(index > _size){
            return;
        }
        else{
            int num = 0;
            ListNode * curr = _dummyHead;
            while(curr->next != nullptr){
                if(num == index){
                    ListNode * newNode = new ListNode(val);
                    newNode->next = curr->next;
                    curr->next = newNode;
                    _size++;                  
                    break;
                }
                num++;
                curr = curr->next;
            }
        }
    }
    
    void deleteAtIndex(int index) {		// 指定位置删除
        int num = 0;
        ListNode * curr = _dummyHead;
        while(curr->next != nullptr){
            if(num == index){
                ListNode * temp = curr->next;
                curr->next = curr->next->next;
                delete temp;
                _size--;
                break;
            }
            num++;
            curr = curr->next;
        }
    }
        
    void printLinkedList() {		// 打印链表
        ListNode* curr = _dummyHead;
        while (curr->next != nullptr) {
            cout << curr->next->val << " ";
            curr = curr->next;
        }
        cout << endl;
    }
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList* obj = new MyLinkedList();
 * int param_1 = obj->get(index);
 * obj->addAtHead(val);
 * obj->addAtTail(val);
 * obj->addAtIndex(index,val);
 * obj->deleteAtIndex(index);
 */
2、双链表 增 删 查

206. 反转链表 ●

1、迭代(双指针法)

在遍历链表时,将当前节点的 next 指针改为指向前一个节点pre。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。
代码随想录

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *curr = head;          // 当前节点
        ListNode *pre = nullptr;        // 前一节点
        ListNode *temp;
        while(curr){  
            temp = curr->next;          // 暂存下一节点
            curr->next = pre;           // 翻转当前节点
            pre = curr;                 // 前一节点后移
            curr = temp;                // 当前节点后移
        }
        return pre;                     // 返回最后一个不会空的节点
    }
};
  • 时间复杂度: O ( n ) O(n) O(n),其中 n 是链表的长度。需要遍历链表一次。
  • 空间复杂度: O ( 1 ) O(1) O(1)

2、递归

  • 从前往后翻转指针指向(与上述双指针类似)
class Solution {
public:
    ListNode *reverse(ListNode *pre, ListNode*curr){
        if(!curr) return pre;                   // 返回最后一个不会空的节点
        ListNode *temp = curr->next;            // 暂存下一节点
        curr->next = pre;                       // 翻转当前节点
        return reverse(curr, temp);
    }
    
    ListNode* reverseList(ListNode* head) {
        return reverse(nullptr, head);                     
    }
};
  • 从后往前翻转指针指向
    在这里插入图片描述
  • 时间复杂度: O ( n O(n O(n),其中 n 是链表的长度。需要对链表的每个节点进行反转操作。
  • 空间复杂度: O ( n ) O(n) O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为 n 层。
class Solution {
public:  
    ListNode* reverseList(ListNode* head) {
        // 空链表 或 尾节点判断
        if(head == nullptr || head->next == nullptr){	
            return head;			
        }
        ListNode *newHead = reverseList(head->next);	// 从后往前反转
        head->next->next = head;						// 实际的反转操作
        head->next = nullptr;	
        return newHead;									// 传递 newHead
    }
};

24. 两两交换链表中的节点 ●●

给两两交换其中相邻的节点,并返回交换后链表的头节点。必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

在这里插入图片描述

1、模拟(无虚拟头节点)

在这里插入图片描述

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if(!head || !head->next) return head;
        int flag = 0;
        ListNode *curr = head;
        ListNode *newHead;
        ListNode *temp;
        while(curr && curr->next){
            if(flag == 0){              // 是否为头结点
                newHead = curr->next;   
            }

            temp = curr->next->next;    // 暂存下一个奇数节点(或空节点)
            curr->next->next = curr;    // 交换
            if(temp){                   
                if(temp->next){         // 下一个偶数节点存在
                    curr->next = temp->next;
                }
                else{
                    curr->next = temp;  
                    break;              // 下一个偶数节点不存在
                }
            }
            else curr->next = temp;     
            curr = temp;                // 当前节点移至下一个奇数节点
            flag = 1;
        }
        return newHead;
    }
};

2、迭代(虚拟头结点模拟)

在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode *curr = dummyHead;
        while(curr->next && curr->next->next){  // 两个临时节点都存在时才执行
            ListNode* temp1 = curr->next;   // 临时节点1
            ListNode* temp2 = temp1->next;  // 临时节点2

            curr->next = temp2;             // 步骤一
            temp1->next = temp2->next;      // 步骤二
            temp2->next = temp1;            // 步骤三
    
            curr = temp1;                   // 指针右移两位
        }
        return dummyHead->next;
    }
};
  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( 1 ) O(1) O(1)

3、递归

终止条件:当前节点为null,或者下一个节点为 null
函数内:将 2 指向 1,1 指向下一层的递归函数,最后返回节点 2
下面中 t 就表示函数内的临时节点 temp,图中节点 1,节点 3 指向的一个片空白,这表示引用关系还没真正确定,要等下一层递归函数返回后,才能真正确定最终指向
在这里插入图片描述

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
    // 如果当前结点为null或当前结点下一个结点为null
    // 则递归终止
    if (head == nullptr || head->next == nullptr)
        return head;

    ListNode* temp = head->next;            // 暂存偶数节点
    head->next = swapPairs(head->next->next);   // 奇数节点指向 下一组数反转后的头结点
    temp->next = head;                      // 反转
    return temp;                            // 返回 该组数反转后的头结点
    }
};
  • 时间复杂度: O ( n ) O(n) O(n),其中 n 是链表的节点数量。需要对每个节点进行更新指针的操作。
  • 空间复杂度: O ( n ) O(n) O(n),其中 n 是链表的节点数量。空间复杂度主要取决于递归调用的栈空间。

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

在这里插入图片描述

1、两次遍历(计算长度)

  • 时间复杂度:O(L),其中 L 是链表的长度。
  • 空间复杂度:O(1)。
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        int length = 0;
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode *curr = dummyHead;
        while(curr->next){          // 一次遍历计算链表长度
            length++;
            curr = curr->next;
        }
        curr = dummyHead;
        int num = 0;
        while(curr->next){          // 二次遍历找出倒数第n个节点
            num++;
            if(num == length-n+1){
                ListNode * temp = curr->next;
                curr->next = temp->next;
                delete temp;
                break;              // 删除后跳出循环
            }
            curr = curr->next;
        }
        return dummyHead->next;
    }
};

2、两次遍历(栈)

在遍历链表的同时将所有节点依次入栈。根据栈「先进后出」的原则,弹出栈的第 n 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        stack<ListNode*> stk;
        ListNode* cur = dummy;
        while (cur) {
            stk.push(cur);				// 遍历入栈
            cur = cur->next;
        }
        for (int i = 0; i < n; ++i) {
            stk.pop();					// 出栈
        }
        ListNode* prev = stk.top();		// 前驱结点(栈顶)
        prev->next = prev->next->next;	// 删除
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};

3、双指针法

如果要删除倒数第 n 个节点,让 fast 先前移 n+1 步,然后让 fast 和 slow 同时移动,直到 fast 指向链表末尾,删掉 slow(前驱节点) 所指向的下一个节点就可以了。
在这里插入图片描述

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode *slow = dummyHead;
        ListNode *fast = dummyHead;
        while(n-- && fast){	
            fast = fast->next;
        }
        fast = fast->next;		// fast 先移动 n+1
        while(fast){			
            slow = slow->next;
            fast = fast->next;
        }
        ListNode *temp = slow->next;
        slow->next = temp->next;
        delete temp;
        return dummyHead->next;
    }
};

面试题 02.07. 链表相交 ●

给你两个单链表的头节点 headA 和 headB ,找出并返回两个单链表相交的起始节点(c1 节点指针)。如两个链表没有交点,返回 null 。
在这里插入图片描述

1、暴力循环

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)

2、计算长度差来减少遍历时间

先遍历长链表,直到剩下的节点数相同,再同时遍历判断。
在这里插入图片描述

  • 时间复杂度: O ( n + m ) O(n + m) O(n+m)
  • 空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode * currA = headA;
        ListNode * currB = headB;
        int lenA = 0;
        int lenB = 0;
       	while(currA){  lenA++;  currA = currA->next; }      // 遍历 计算链表A长度   
        while(currB){  lenB++;  currB = currB->next; }      // 遍历 计算链表B长度
        currA = headA;
        currB = headB;
        // 先遍历较长的链表,至剩下节点相同
        if(lenA > lenB){  while(lenA - lenB > 0)  { currA = currA->next;} } 
        if(lenB > lenA){  while(lenB - lenA > 0)  { currB = currB->next;} } 
        // 剩下的节点A与B同时遍历并判断
        while(currA){
            if(currA == currB){
                return currA;
            }
            currB = currB->next;    
            currA = currA->next;
        }
        return NULL;
    }
};

3、双指针(数学技巧)

在这里插入图片描述

  • 指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为:a + (b - c)

  • 指针 B 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为:b + (a - c)

  • 此时指针 A , B 重合:

    • 若两链表 公共尾部 (即 c >0 ) :指针 A , B 同时指向「第一个公共节点」node 。
    • 若两链表 公共尾部 (即 c =0 ) :指针 A , B 同时指向 null 。
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode * currA = headA;
        ListNode * currB = headB;
        while(currA != currB){
            currA = (currA != NULL)? currA->next : headB;	// 当前currA为空(尾节点)时,则指向 headB
            currB = (currB != NULL)? currB->next : headA;	// 当前currB为空(尾节点)时,则指向 headA
        }
        return currA;
    }
};

142. 环形链表 II ●●

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
在这里插入图片描述

1、哈希表

  • 时间复杂度: O ( N ) O(N) O(N),其中 N 为链表中节点的数目。我们恰好需要访问链表中的每一个节点。

  • 空间复杂度: O ( N ) O(N) O(N),其中 N 为链表中节点的数目。我们需要将链表中的每个节点都保存在哈希表当中。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        unordered_set<ListNode*> hash;
        ListNode * curr = head;
        while(curr){
            if(hash.count(curr)){
                return curr;
            }
            else{
                hash.insert(curr);		
                curr = curr->next;
            }   
        }
        return NULL;
    }
};

2、双指针法

在这里插入图片描述

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode * slow = head;
        ListNode * fast = head;
        while(fast && fast->next){			// 无环即退出循环
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast){				// 第一次相遇
                ListNode * index1 = fast;	// 更新指针
                ListNode * index2 = head;
                while(index1 != index2){	// 入口节点相遇
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index1;
            }
        }
        return NULL;
    }
};

2. 两数相加 ●●

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

模拟(同时遍历两个链表,相加并建立新的节点,当某链表遍历完后,相加值置0,注意末尾进位)

  • 时间复杂度: O ( m a x ( m , n ) ) O(max(m,n)) O(max(m,n)),其中 m 和 n 分别为两个链表的长度。 我们要遍历两个链表的全部位置,而处理每个位置只需要 O(1) 的时间。
  • 空间复杂度: O ( 1 ) O(1) O(1)。 注意返回值不计入空间复杂度。
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        int flag = 0;								// 进位标记
        ListNode * dummyHead = new ListNode(0);		// 返回头结点 一般建立虚拟头结点
        ListNode * curr = dummyHead;				// curr表当前节点
        while(l1 || l2){							// 有一个链表不为空则继续遍历
            int val1 = l1? l1->val : 0;				// 该链表已遍历完时,加值为0.
            int val2 = l2? l2->val : 0;
            int sum = val1 + val2 + flag;			// 求和
            curr->next = new ListNode(sum % 10);	// 创建新节点,并赋值
            curr = curr->next;						// 移动当前节点
            flag = sum / 10;						// 进位标记
            if(l1) l1 = l1->next;					// 移动原链表
            if(l2) l2 = l2->next;
        }
        if(flag){									// 当末尾还有进位时,创建新节点,并赋值1
            curr->next = new ListNode(flag);		// 999 + 1 = 1000
        }
        return dummyHead->next;						// 返回真实头结点
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值