链表简介
链表是一种数据结构,其中的元素都是链接在一起的。每个元素包含数据以及指向下一个元素的指针。基于这种结构,链表的元素可以容易地插入或删除,而不需要重新安排或移动整个列表,这与数组不同。
常见的链表有以下几种:
- 单链表:每个元素只有一个指向下一个元素的指针。
- 双链表:每个元素有两个指针,一个指向下一个元素,另一个指向前一个元素。
- 循环链表:链表的最后一个元素指向链表的第一个元素。
实现方法
1.最简单的单链表
#include <iostream>
using namespace std;
// Definition for class Node from lecture slides
class Node {
public:
Node* next;
double data;
Node(double val) :data(val), next(nullptr) {};
};
class List {
public:
List(void) { head = NULL; } // constructor
~List(void); // destructor
bool IsEmpty() { return head == NULL; }
Node* InsertNode(int index, double x);
int FindNode(double x);
int DeleteNode(double x);
void DisplayList(void);
private:
Node* head;
};
List::~List(void) {
Node* currNode = head, * nextNode = NULL;
while (currNode != NULL)
{
nextNode = currNode->next;
// destroy the current node
delete currNode;
currNode = nextNode;
}
}
// Function definition for InsertNode
Node* List::InsertNode(int index, double x) {
if (index < 0)return nullptr;
int currdex = 1;
Node* curr = head;
while (curr && currdex < index) {
curr = curr->next;
}
if (index > 0 && curr == nullptr)return nullptr;
Node* newnode = new Node(x);
if (index == 0) {
newnode->next = head;
head = newnode;
}
else {
newnode->next = curr->next;
curr->next = newnode;
}
return newnode;
}
// Function definition for FindNode
int List::FindNode(double x) {
Node* curr = head;
int currindex = 1;
while (curr && curr->data != x) {
curr = curr->next;
currindex++;
}
if (curr)return currindex-1;
return -1;
}
// Function definition for DeleteNode
int List::DeleteNode(double x) {
if (!head)return -1;
if (head->data == x) {
Node* temp = head;
head = head->next;
delete temp;
return 0; // index 0
}
Node* curr = head;
int currdex = 1;
while (curr && curr->next->data != x) {
curr = curr->next;
currdex++;
}
if (curr->next != nullptr) {
Node* todelete = curr->next;
curr->next = todelete->next;
delete todelete;
}
if(curr)return currdex;
return -1;
}
// Function definition for DisplayList
void List::DisplayList() {
Node* curr = head;
while (curr) {
cout << curr->data << ' ';
curr = curr->next;
}
}
int main()
{
// Function calls :: copy from lecture slides
List list;
list.InsertNode(0,1);
list.InsertNode(0,2);
list.InsertNode(0,3);
list.InsertNode(0,4);
list.DisplayList();
list.DeleteNode(2);
list.DisplayList();
list.DeleteNode(4);
list.DisplayList();
return 0;
}
2.STL库中的list
STL库中的list
是一个双向链表。它提供了一系列功能,使得在列表的前面和后面插入或删除元素变得非常高效。由于其为双向链表,因此任何位置的插入、访问或删除都是线性时间复杂度,但与vector
相比,它具有在头部快速插入和删除的优势。
#include <iostream>
#include <list>
int main() {
// 1. 初始化
std::list<int> l1; // Empty list of ints
std::list<int> l2(4, 100); // Four ints with value 100
std::list<int> l3(l2.begin(), l2.end()); // A copy of l2
// 输出初始化后的l2
std::cout << "List l2 after initialization: ";
for (int num : l2) {
std::cout << num << " ";
}
std::cout << std::endl;
// 2. 插入元素
l1.push_back(1); // Inserts 1 at the end
l1.push_front(0); // Inserts 0 at the beginning
// 3. 访问元素
int first = l1.front(); // Gets the first element
int last = l1.back(); // Gets the last element
std::cout << "First element of l1: " << first << std::endl;
std::cout << "Last element of l1: " << last << std::endl;
// 4. 删除元素
l1.pop_back(); // Removes the last element
l1.pop_front(); // Removes the first element
// 5. 迭代器
std::cout << "Elements in l1 using iterators: ";
std::list<int>::iterator it = l1.begin(); // Get iterator to the beginning
for (; it != l1.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 6. 其他有用的函数
std::cout << "Size of l1: " << l1.size() << std::endl; // Returns the number of elements
std::cout << "Is l1 empty?: " << l1.empty() << std::endl; // Checks if the list is empty
l2.reverse(); // Reverses l2
std::cout << "l2 after reverse: ";
for (int num : l2) {
std::cout << num << " ";
}
std::cout << std::endl;
l2.sort(); // Sorts l2
std::cout << "l2 after sort: ";
for (int num : l2) {
std::cout << num << " ";
}
std::cout << std::endl;
l2.remove(100); // Removes all the elements with value 100
std::cout << "l2 after removing all instances of 100: ";
for (int num : l2) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
链表习题
力扣上链表的题要敢于多开空间记录节点位置,节点的先后顺序很重要
1.链表的遍历
(1)链表的分割
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
ListNode* lessnode = new ListNode(-1);
ListNode* greatnode = new ListNode(-1);
ListNode* curr1 = lessnode;
ListNode* curr2 = greatnode;
ListNode* curr = head;
while (curr) {
if (curr->val < x) {
curr1->next = curr;
curr1 = curr1->next;
} else {
curr2->next = curr;
curr2 = curr2->next;
}
curr = curr->next;
}
curr1->next = greatnode->next;
curr2->next = nullptr; // 确保链表结尾
ListNode *result = lessnode->next;
delete lessnode; // 释放临时创建的头结点
delete greatnode;
return result;
}
};
(2)交换链表中的节点
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
这个题可以简单的遍历链表来解决,然后交换节点的值(有点水)
class Solution {
public:
ListNode* swapNodes(ListNode* head, int k) {
ListNode*temp=head;
ListNode*t2=head;
int n=0;
while(temp!=nullptr){
n++;
temp=temp->next;
}
temp=head;
for(int i=0;i<k-1;i++){
temp=temp->next;
}
for(int i=0;i<n-k;i++){
t2=t2->next;
}
int exc=0;
exc=temp->val;
temp->val=t2->val;
t2->val=exc;
return head;
}
};
(3)复制带随机指针的链表
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
这道题的难点就在于随机指针的复制,我第一次想到的是先不管随机指针只是拷贝链表,然后在通过搜索随机指针的值来复制随机指针。然而search
方法试图通过值来找到 random
指针应该指向的节点。但如果有多个节点具有相同的值,这种方法就会出错,因为它总是返回第一个匹配的节点,这可能不是正确的 random
目标。
class Solution {
public:
Node*search(Node*head,int val){
Node*curr=head;
while(curr){
if(curr->val==val){
return curr;
}
curr=curr->next;
}
return nullptr;
}
Node* copyRandomList(Node* head) {
if(!head)return nullptr;
Node*ans=new Node(head->val);
Node*ptr=ans;
Node*curr=head->next;
while(curr){
ptr->next=new Node(curr->val);
curr=curr->next;
ptr=ptr->next;
}
curr=head;
ptr=ans;
while(curr){
if(curr->random){
ptr->random=search(ans,curr->random->val);
}
curr=curr->next;
ptr=ptr->next;
}
return ans;
}
};
解决这个问题通常需要建立原始节点与其副本之间的映射关系。在复制列表的同时,我们可以使用哈希表记录原始节点和新节点之间的对应关系,然后在第二遍遍历中将新的链表链接起来。这种方法的空间复杂度和时间复杂度都是O(n)。
class Solution {
public:
Node* copyRandomList(Node* head) {
if(!head)return nullptr;
unordered_map<Node*,Node*>map;
Node*curr=head;
//通过遍历建立新链表和旧链表之间的哈希映射
while(curr){
map[curr]=new Node(curr->val);
curr=curr->next;
}
curr=head;
//二次遍历将新的链表链接起来
while(curr){
map[curr]->next=map[curr->next];
map[curr]->random=map[curr->random];
curr=curr->next;
}
return map[head];
}
};
官方题解给了一种通过迭代和拆分节点方法来节约开哈希表的空间,个人觉得方法有点特别想不到。
class Solution {
public:
Node* copyRandomList(Node* head) {
if (head == nullptr) {
return nullptr;
}
//在每个节点后面添加复制节点
for (Node* node = head; node != nullptr; node = node->next->next) {
Node* nodeNew = new Node(node->val);
nodeNew->next = node->next;
node->next = nodeNew;
}
//复制节点的随机指针指向的是原节点指向随机节点的复制节点
for (Node* node = head; node != nullptr; node = node->next->next) {
Node* nodeNew = node->next;
nodeNew->random = (node->random != nullptr) ? node->random->next : nullptr;
}
Node* headNew = head->next;
//断开复制节点和原节点的链接
for (Node* node = head; node != nullptr; node = node->next) {
Node* nodeNew = node->next;
node->next = node->next->next;
nodeNew->next = (nodeNew->next != nullptr) ? nodeNew->next->next : nullptr;
}
return headNew;
}
};
2.链表反转
反转链表总结起来大概是四种方法基本上可以解决简单的反转链表的题
//1.递归
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;
}
};
//2.双指针
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==nullptr||head->next==nullptr)return head;
ListNode*pre=head;
ListNode*curr=pre->next;
while(curr){
ListNode*next=curr->next;
curr->next=pre;
pre=curr;
curr=next;
}
head->next=nullptr;
return pre;
}
};
//3.哈希表
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head||!head->next)return head;
unordered_map<ListNode*,ListNode*>hash;
hash[head]=nullptr;
ListNode*curr=head;
while(curr->next){
hash.insert({curr->next,curr});
curr=curr->next;
}
ListNode*newhead=curr;
while(curr){
curr->next=hash[curr];
curr=curr->next;
}
head->next=nullptr;
return newhead;
}
};
//4.栈
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head||!head->next)return head;
ListNode*curr=head;
stack<ListNode*>stk;
while(curr){
stk.push(curr);
curr=curr->next;
}
ListNode*newhead=stk.top();
stk.pop();
curr=newhead;
while(!stk.empty()){
curr->next=stk.top();
curr=curr->next;
stk.pop();
}
curr->next=nullptr;
return newhead;
}
};
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
3.旋转链表
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
我结合了反转链表的题型,找到要旋转的位置之后进行三次反转。题解中貌似没有给出这种解法,
现在,让我们通过一个简单的图示来解释这个过程。假设我们有一个链表 1->2->3->4->5
并且 k=2
。
原始链表:
1 -> 2 -> 3 -> 4 -> 5
旋转链表,我们需要把尾部的两个元素 4
和 5
移动到前面来。
新链表应该看起来像这样:
4 -> 5 -> 1 -> 2 -> 3
过程中的步骤是这样的:
- 计算链表的大小
size
(在这个例子中是5)。 - 确定实际需要的旋转步数
k = k % size
(在这个例子中是2)。 - 找到新的尾部,它将是第
size - k
个节点(在这个例子中是3
)。 - 新的头是当前尾部的下一个节点,也就是
4
。 - 将链表的两部分分别反转
- 反转结束后把链表的两个部分重新连接起来。
- 将链接起来的新链表再次整体反转
class Solution {
public:
// This function reverses the list from head up to the node before tail
ListNode* rotate(ListNode* head, ListNode* tail) {
if (!head || !head->next) return head;
ListNode *pre = nullptr, *curr = head, *next = nullptr;
// Reverse the list until we reach the tail.
// The tail node is not included in the reversal.
while (curr != tail) {
next = curr->next;
curr->next = pre;
pre = curr;
curr = next;
}
// At the end, 'pre' is the new head of the reversed list.
return pre;
}
// This function rotates the list to the right by 'k' places.
ListNode* rotateRight(ListNode* head, int k) {
if (!head || k == 0) return head;
// First, we compute the size of the list.
int size = 0;
ListNode* curr = head;
while (curr) {
curr = curr->next;
size++;
}
if (size == 0) return head;
// The actual number of rotations needed is 'k' modulo the list size.
k = k % size;
// If the number of rotations is 0, then we return the list as it is.
if (k == 0) return head;
// Find the new tail of the rotated list, which will be 'size - k' steps from the beginning.
curr = head;
for (int i = 0; i < size - k - 1; ++i) {
curr = curr->next;
}
// The node next to the new tail is the new head of the rotated list.
ListNode* newHead = curr->next;
// Disconnect the new tail from the rest of the list.
curr->next = nullptr;
// Reverse the first part of the list from the old head to the new tail.
ListNode* prehead = rotate(head, newHead);
// Reverse the second part of the list which starts with newHead.
newHead = rotate(newHead, nullptr);
// Connect the first part with the second part.
head->next = newHead;
// Return the new head after reversing the first part again to restore the original order.
return rotate(prehead, nullptr);
}
};
4.删除链表中的节点
删除链表中的某个节点最简单的方法就是单指针找到要删除的节点的前一个节点
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
if(!head)return nullptr;
if(head->val==val)return head->next;
ListNode*curr=head;
ListNode*toDelete=curr;
while(curr->next->val!=val){
curr=curr->next;
}
toDelete=curr->next;
curr->next=toDelete->next;
toDelete->next=nullptr;
delete toDelete;
return head;
}
};
这个删除节点的题挺有意思的,不给头节点的情况下只能是用后面节点的值来覆盖
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
class Solution {
public:
void deleteNode(ListNode* node) {
ListNode*curr=node;
while(curr->next->next){
curr->val=curr->next->val;
curr=curr->next;
}
curr->val=curr->next->val;
curr->next=nullptr;
}
};