链表相关问题的多种解法(最小堆、双指针、栈)

链表相关问题的多种解法(最小堆、双指针、栈)

在本文中,我们将深入探讨几道经典的链表相关的算法题,这些题目涵盖了不同的技巧,包括双指针、虚拟头结点、栈以及最小堆的运用。

24. 两两交换链表中的节点(虚拟头结点)

题目描述

给定一个链表,我们需要两两交换其中相邻的节点,并且不能修改节点内部的值,只能通过节点交换来完成,最后返回交换后链表的头节点。

示例分析

  • 示例 1:输入 head = [1,2,3,4],期望输出 [2,1,4,3]。这里我们可以看到,链表中的节点 1 和 2 进行了交换,3 和 4 也进行了交换。
  • 示例 2:输入 head = [],输出 []。空链表的情况比较简单,直接返回空即可。
  • 示例 3:输入 head = [1],输出 [1]。当链表只有一个节点时,无需进行交换,直接返回原节点。

代码解析

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        // 创建一个虚拟头结点,它的值为0,next指针指向原链表的头节点。
        // 这样做的好处是可以方便地处理头节点可能发生变化的情况,避免了特殊情况的单独处理。
        ListNode dummyNode(0);
        dummyNode.next = head;
        // 定义一个指针p,初始指向虚拟头结点。
        ListNode* p = &dummyNode;
        // 当p的下一个节点和下下个节点都不为空时,进入循环进行交换操作。
        while (p->next!= nullptr && p->next->next!= nullptr) {
            // 临时保存需要交换的节点。
            ListNode* temp1 = p->next;
            ListNode* temp2 = p->next->next;
            ListNode* temp3 = p->next->next->next;
            // 进行节点交换操作。
            // 首先将p的next指针指向temp2,即原来的第二个节点。
            p->next = p->next->next;
            // 然后将temp2的next指针指向temp1,即原来的第一个节点。
            temp2->next = temp1;
            // 最后将temp1的next指针指向temp3,即原来的第三个节点(交换后的下一组的第一个节点)。
            temp1->next = temp3;
            // 将指针p移动到交换后的下一组的第一个节点的前一个节点,即temp2。
            p = p->next->next;
        }
        // 返回交换后的链表的头节点,即虚拟头结点的下一个节点。
        return dummyNode.next;
    }
};

82. 删除排序链表中的重复元素 II(前后指针)

题目描述

给定一个已排序的链表的头 head,需要删除原始链表中所有重复数字的节点,只留下不同的数字,最后返回已排序的链表。

示例分析

  • 示例 1:输入 head = [1,2,3,3,4,4,5],输出 [1,2,5]。这里的 3 和 4 都有重复节点,需要将它们全部删除。
  • 示例 2:输入 head = [1,1,1,2,3],输出 [2,3]。这里的 1 重复了多次,需要删除所有重复的 1。

代码解析

class Solution {//因为是有序的,所以直接遍历就行,不需要哈希
public:
    ListNode* deleteDuplicates(ListNode* head) {
        // 创建一个虚拟头结点,其next指针指向原链表的头节点。
        ListNode dummyNode(0);
        dummyNode.next = head;
        // 定义两个指针,p1指向原链表的头节点(虚拟头结点的下一个节点),p2指向虚拟头结点。
        ListNode* p1 = dummyNode.next;
        ListNode* p2 = &dummyNode;
        // 如果链表为空或者只有一个节点,直接返回原链表的头节点(即虚拟头结点的下一个节点)。
        if (p1 == nullptr || p1->next == nullptr) {
            return dummyNode.next;
        }
        // 当p1和p1的下一个节点都不为空时,进入循环。
        while (p1!= nullptr && p1->next!= nullptr) {
            // 如果p1的值和p1的下一个节点的值相等,说明找到了重复的节点。
            if (p1->val == p1->next->val) {
                // 继续向后遍历,直到找到下一个不相等的节点。
                while (p1->next!= nullptr && p1->val == p1->next->val) {
                    p1 = p1->next;
                }
                // 将p2的next指针指向p1的下一个节点,跳过所有重复的节点。
                p2->next = p1->next;
                // 将p1移动到下一个不相等的节点。
                p1 = p1->next;
            } else {
                // 如果p1和p1的下一个节点的值不相等,将p1和p2都向后移动一位。
                p1 = p1->next;
                p2 = p2->next;
            }
        }
        // 返回处理后的链表的头节点,即虚拟头结点的下一个节点。
        return dummyNode.next;
    }
};

1836. 从未排序的链表中移除重复元素(哈希 + 双指针)

题目描述

给定一个链表的第一个节点 head,找到链表中所有出现多于一次的元素,并删除这些元素所在的节点,最后返回删除后的链表。

示例分析

  • 示例 1:输入 head = [1,2,3,2],输出 [1,3]。这里的 2 出现了两次,需要删除所有的 2。
  • 示例 2:输入 head = [2,1,1,2],输出 []。这里的 1 和 2 都出现了两次,需要删除所有的节点。
  • 示例 3:输入 head = [3,2,2,1,3,2,4],输出 [1,4]。这里的 3 出现了两次,2 出现了三次,需要删除所有的 3 和 2。

代码解析

#include<iostream>
#include <limits>
#include <unordered_map>
using namespace std;
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){}
};
// 创建链表节点的函数
ListNode* creatNode() {
    ListNode* tail = nullptr;
    ListNode* head = nullptr;
    int num;
    while (cin >> num) {
        ListNode* newnode = new ListNode(num);
        if (head == nullptr) {
            head = newnode;
            tail = newnode;
        } else {
            tail->next = newnode;
            tail = newnode;
        }
    }
    cin.clear();
    cin.ignore(numeric_limits<streamsize>::max(), '\n');
    tail->next = nullptr;
    return head;
}
// 打印链表节点的函数
void printNode(ListNode* head) {
    ListNode* p = head;
    while (p!= nullptr) {
        if (p->next!= nullptr) {
            cout << p->val << " -> ";
        } else {
            cout << p->val;
        }
        p = p->next;
    }
    cout << endl;
}
ListNode* deleteDuplicatesUnsorted(ListNode* head) {
    // 使用哈希表来记录每个元素出现的次数。
    unordered_map<int, int> map;
    ListNode dummyNode(0);
    dummyNode.next = head;
    ListNode* p1 = dummyNode.next;
    // 先遍历一遍链表,将每个元素出现的次数记录在哈希表中。
    while (p1!= nullptr) {
        map[p1->val]++;
        p1 = p1->next;
    }
    ListNode* p2 = dummyNode.next;
    ListNode* p3 = &dummyNode;
    // 再次遍历链表,对于出现次数大于1的元素,删除其所在的节点。
    while (p2!= nullptr) {
        if (map[p2->val] > 1) {
            p3->next = p2->next;
            p2 = p2->next;
        } else {
            p2 = p2->next;
            p3 = p3->next;
        }
    }
    p3->next = nullptr;
    return dummyNode.next;
}
int main() {
    cout << "请按照头尾节点顺序依次输入节点值:" << endl;
    ListNode* inNode = creatNode();
    ListNode* deldnode = deleteDuplicatesUnsorted(inNode);
    cout << "经过处理后的链表是:" << endl;
    printNode(deldnode);
    return 0;
}

378. 有序矩阵中第 K 小的元素(最小堆)

题目描述

给定一个 n x n 矩阵 matrix,其中每行和每列元素均按升序排序,需要找到矩阵中第 k 小的元素。注意,这里是排序后的第 k 小元素,而不是第 k不同的元素,并且要找到一个内存复杂度优于 O(n2) 的解决方案。

示例分析

  • 示例 1:输入 matrix = [[1,5,9],[10,11,13],[12,13,15]]k = 8,输出 13。矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13。
  • 示例 2:输入 matrix = [[-5]]k = 1,输出 -5

代码解析

class Solution {
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {
        // 通过设置最小堆,来获取那一列内容最小值。
        // 这里定义了一个比较函数,用于确定堆中元素的顺序。
        auto cmp = [] (vector<int> left, vector<int> right) {return left[0] > right[0];};
        // 设置为vector<int>来存放对应元素和其行号列号,方便传递给后一位节点。
        priority_queue<vector<int>, vector<vector<int>>,decltype(cmp)> minheap(cmp);
        // 将矩阵每一行的第一个元素加入最小堆。
        for (int i = 0; i < matrix.size(); i++) {
            minheap.push({matrix[i][0], i, 0});
        }
        int result = -1;
        // 当堆不为空且还没有找到第k小的元素时,继续循环。
        while (!minheap.empty() && k--) {
            vector<int> min = minheap.top();
            minheap.pop();
            result = min[0];
            int i = min[1], j = min[2];
            // 如果当前元素所在列不是最后一列,则将其下一列的元素加入堆中。
            if (j < matrix.size() - 1) {
                minheap.push({matrix[i][j + 1], i, j + 1});
            }
        }
        return result;
    }
};

373. 查找和最小的 K 对数字(最小堆)

题目描述

给定两个以非递减顺序排列的整数数组 nums1nums2,以及一个整数 k。定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2。需要找到和最小的 k 个数对 (u1,v1)(u2,v2),…,(uk,vk)

示例分析

  • 示例 1:输入 nums1 = [1,7,11]nums2 = [2,4,6]k = 3,输出 [1,2],[1,4],[1,6]。这里返回的是和最小的前三对数。
  • 示例 2:输入 nums1 = [1,1,2]nums2 = [1,2,3]k = 2,输出 [1,1],[1,1]

代码解析

class Solution {
public:
    vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
        // 定义一个比较函数,用于确定堆中元素的顺序,这里按照数对的和来比较。
        auto cmp = [](vector<int>& a, vector<int>& b) {
            return (a[0] + a[1]) > (b[0] + b[1]);
        };
        // 创建一个最小堆。
        priority_queue<vector<int>,vector<vector<int>>,decltype(cmp)> minheap(cmp);
        // 将nums1里的所有元素和nums2[0]作为一对放入最小堆。
        for (int i = 0; i < nums1.size() && i < k; i++) {
            minheap.push({nums1[i],nums2[0],0});
        }
        vector<vector<int>> result;
        // 当堆不为空且还没有找到k个数对时,继续循环。
        while (!minheap.empty() && k--) {
            vector<int> cur = minheap.top();
            minheap.pop();
            result.push_back({cur[0], cur[1]});
            int j = cur[2] + 1;
            // 如果nums2中还有未使用的元素,则将当前nums1中的元素和下一个nums2中的元素组成数对放入堆中。
            if (j < nums2.size()) {
                minheap.push({cur[0], nums2[j], j});
            }
        }
        return result;
    }
};

2. 两数相加(双指针)

题目描述

给定两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。需要将两个数相加,并以相同形式返回一个表示和的链表。可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例分析

  • 示例 1:输入 l1 = [2,4,3]l2 = [5,6,4],输出 [7,0,8]。这里表示的是 342 + 465 = 807。
  • 示例 2:输入 l1 = [0]l2 = [0],输出 [0]
  • 示例 3:输入 l1 = [9,9,9,9,9,9,9]l2 = [9,9,9,9],输出 [8,9,9,9,0,0,0,1]

代码解析

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        // 在两条链表上的指针,分别初始化为两条链表的头节点。
        ListNode *p1 = l1, *p2 = l2;
        // 虚拟头结点(构建新链表时的常用技巧),其值为 -1。
        ListNode *dummy = new ListNode(-1);
        // 指针p负责构建新链表,初始指向虚拟头结点。
        ListNode *p = dummy;
        // 记录进位,初始化为0。
        int carry = 0;
        // 开始执行加法,两条链表走完且没有进位时才能结束循环。
        while (p1!= nullptr || p2!= nullptr || carry > 0) {
            // 先加上上次的进位。
            int val = carry;
            if (p1!= nullptr) {
                val += p1->val;
                p1 = p1->next;
            }
            if (p2!= nullptr) {
                val += p2->val;
                p2 = p2->next;
            }
            // 处理进位情况,计算新的进位。
			// 如果 val 大于等于 10,进位为 1,否则为 0。
            carry = val / 10;
            // 取个位数字作为新节点的值。
            val = val % 10;
            // 构建新节点,将其加入到结果链表中。
            p->next = new ListNode (val);
            // 移动指针 p 到新节点。
            p = p->next;
            }
            // 返回结果链表的头结点(去除虚拟头结点)。
            return dummy->next;
          }
        };

445. 两数相加 II (栈中转)

题目描述

给定两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。可以假设除了数字 0 之外,这两个数字都不会以零开头。

示例分析

  • 示例 1:输入l1 = [7,2,4,3]l2 = [5,6,4],输出[7,8,0,7]
  • 示例 2:输入l1 = [2,4,3]l2 = [5,6,4],输出[8,0,7]
  • 示例 3:输入l1 = [0]l2 = [0],输出[0]

代码解析

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        // 创建两个栈,分别用于存储两个链表中的数字。
        stack<int> str1,str2;
        ListNode* p1 = l1;
        ListNode* p2 = l2;
        // 将链表 l1 中的数字依次压入栈 str1。
        while(p1!=nullptr){
            str1.push(p1->val);
            p1=p1->next;
        }
        // 将链表 l2 中的数字依次压入栈 str2。
        while(p2!=nullptr){
            str2.push(p2->val);
            p2=p2->next;
        }
        // 创建一个虚拟头结点,值为 -1。
        ListNode* dummy = new ListNode(-1);
        int carry = 0;
        // 当两个栈都不为空时,进行加法运算。
        while(!str1.empty()||!str2.empty()){
            // 如果栈 str1 为空,取 0 作为当前数字,否则取栈顶数字并弹出。
            int a = str1.empty()? 0 : str1.top();
            if (!str1.empty()) str1.pop();
            // 如果栈 str2 为空,取 0 作为当前数字,否则取栈顶数字并弹出。
            int b = str2.empty()? 0 : str2.top();
            if (!str2.empty()) str2.pop(); 
            // 计算两个数字和进位的和。
            int sum = a + b + carry;
            // 计算新的进位。
            carry = sum / 10;
            // 创建一个新节点,值为 sum % 10。
            ListNode* newnode = new ListNode(sum % 10);
            // 将新节点插入到虚拟头结点之后,新节点成为结果链表的头节点。
            newnode->next = dummy->next;
            dummy->next = newnode;
        }
        // 如果最后还有进位,创建一个新节点,值为进位,插入到结果链表头部。
        if(carry>0){
            ListNode* newnode = new ListNode(carry);
            newnode->next = dummy->next;
            dummy->next = newnode;
        }
        // 返回结果链表的头节点(虚拟头结点的下一个节点)。
        return dummy->next;
    }
};

25. K 个一组翻转链表(递归)

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

题目描述

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例分析

  • 示例 1

    输入:head = [1,2,3,4,5], k = 2
    输出:[2,1,4,3,5]
    
  • 示例 2

    输入:head = [1,2,3,4,5], k = 3
    输出:[3,2,1,4,5]
    

代码解析

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        if(head==nullptr) return nullptr;
        ListNode *a=head, *b=head;
        for(int i=0; i<k; i++){
            if(b==nullptr) return head;//如果有空就return到递归上一级
            b = b->next;//指向下一轮‘含k个元素子段’的第一个元素
        }
        ListNode* newhead = reverseNode(a, k);//先反转本轮前k个元素,因为反转后的a就变成了末尾元素
        a->next=reverseKGroup(b, k);//递归调用,反转后的a再连接到下一轮元素中(返回的newhead)
        return newhead;
    }
    //反转前n个节点
    ListNode* reverseNode(ListNode* head, int n){
        ListNode *pre=nullptr, *cur=head;
        while(n-->0&&cur!=nullptr){
            ListNode *temp=cur->next;
            cur->next=pre;
            pre=cur;
            cur=temp;
        }
        return pre;//返回反转后的队头元素
    }
};

通过对这些链表相关算法题的分析和代码讲解,我们可以看到不同的解题思路和技巧在处理链表问题中的应用。这些技巧包括利用虚拟头结点简化操作、使用双指针遍历链表、借助栈来处理逆序问题以及利用最小堆来优化查找过程等。希望这些内容能帮助大家更好地理解和解决链表相关的算法问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值