List
链表
1.如何构造一个链表?
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null。链表的入口节点称为链表的头结点也就是head。
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
int main(){
ListNode *head = new ListNode(1); // 创建头节点
ListNode *second = new ListNode(2); // 创建第二个节点
ListNode *third = new ListNode(3); // 创建第三个节点
head->next = second; // 将头节点的next指向第二个节点
second->next = third; // 将第二个节点的next指向第三个节点
third->next = NULL; // 将第三个节点的next指向NULL
cout<<head->val<<" -> "<<head->next->val<<" -> "<<head->next->next->val<<endl; // 输出链表的值
// 释放内存
delete head;
delete second;
delete third;
return 0; // 程序结束
}
- 链表节点结构体 (ListNode):
int val
: 存储节点的值。ListNode *next
: 指向下一个节点的指针。- 构造函数
ListNode(int x)
: 初始化节点值和将next指针设为NULL。
- main函数中的链表创建:
- 创建头节点
head
,值为1。 - 创建第二个节点
second
,值为2。 - 创建第三个节点
third
,值为3。 - 通过
next
指针连接这些节点:head->next = second。
second->next = third
。third->next = NULL
(表示链表结束)。
- 创建头节点
链表有很多种,上图的单链表是一种,还有双链表,循环链表,循环双链表,如STL库里的list就是循环双链表。
2.删除链表中元素
删除链表中等于给定值 val 的所有节点。以链表1->4->2->4 来举例,移除元素4。
删除4这个元素,只需要判断current->next->val是否等于4,如果相等,current->next=current->next->next,然后在内存中删掉4这个节点,如果不是,那就current=curenr->next,移动到下一个位置,最后返回head。
如果是删除1这个元素,1是头节点,这里的逻辑就和删除中间节点不一样。
如图,应该是将head移动到head->next这个位置,然后在内存中删掉第一个节点。
如果我们需要统一一个逻辑在做这道题,需要一个虚拟的头节点。
这样不管是删掉第一个还是其他的元素,都是用的一个逻辑。
#include<iostream>
using namespace std;
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
//直接对链表进行操作
ListNode* removeElements(ListNode* head,int val){
while(head !=NULL && head->val==val){
ListNode* temp=head;//保留头节点
head=head->next;//将头节点指向下一个节点
delete temp;//释放头节点的内存
}
ListNode* current=head;//当前节点指针
while(current !=NULL && current->next !=NULL){
if(current->next->val==val){
ListNode* temp=current->next; // 保留要删除的节点
current->next=current->next->next; // 删除当前节点的下一个节点
delete temp; // 释放内存
}else{
current=current->next;
}
}
return head; // 返回新的头节点
}
//利用虚拟头节点删除
ListNode* removeElementsWithDummy(ListNode* head,int val){
ListNode* dummy=new ListNode(0);//这是一个虚拟的头节点
dummy->next=head;
ListNode* current=dummy;
while(current !=NULL && current->next !=NULL){
if(current->next->val==val){
ListNode* temp=current->next; // 保留要删除的节点
current->next=current->next->next; // 删除当前节点的下一个节点
delete temp; // 释放内存
}else{
current=current->next;
}
}
//如果直接这样返回的话,虚拟的没有得到释放,会造成内存管理的混乱
//return dummy->next;
head = dummy->next;
delete dummy;
return head;
}
int main(){
// 创建链表
ListNode *head = new ListNode(1);
ListNode *second = new ListNode(2);
ListNode *third = new ListNode(6);
ListNode *fourth = new ListNode(3);
ListNode *fifth = new ListNode(4);
ListNode *sixth = new ListNode(5);
ListNode *seventh = new ListNode(6);
head->next = second;
second->next = third;
third->next = fourth;
fourth->next = fifth;
fifth->next = sixth;
sixth->next = seventh;
seventh->next = NULL;
// 删除值为6的节点
//head = removeElements(head, 1);
head = removeElementsWithDummy(head, 6);
// 输出链表
ListNode* current = head;
while(current != NULL){
cout << current->val << " ";
current = current->next;
}
// 释放内存
current = head;
while(current != NULL){
ListNode* temp = current;
current = current->next;
delete temp;
}
return 0; // 程序结束
}
3.设计链表
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
第四点的逻辑如下图所示:
先将一个东西移动到index-1处,然后current->next=addelem,addelem>next = current->next。
最后一个删除指定位置的index。
#include<iostream>
using namespace std;
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
class MyLinkedList {
private:
ListNode* head; // 链表的头节点
int size; // 链表的大小
public:
MyLinkedList() {
head =new ListNode(0); // 初始化头节点为虚拟节点
size = 0; // 初始化链表大小为0
}
int get(int index) {
if(index<0||index>=size){
return -1;
}
ListNode* current=head->next;
for(int i=0;i<index;i++){
current=current->next; // 遍历到指定索引的节点
}
return current->val; // 返回节点的值
}
void addAtHead(int val) {
addAtIndex(0, val); // 在头部添加节点
}
void addAtTail(int val) {
addAtIndex(size, val); // 在尾部添加节点
}
void addAtIndex(int index, int val) {
if(index > size) {
cout << "Index out of bounds, node not added." << endl;
return; // 如果索引超出范围,返回
}
if(index < 0) {
index = 0; // 如果index小于0,则在头部插入节点
}
ListNode* current = head;
for(int i = 0; i < index; i++) {
current = current->next; // 遍历到指定索引的前一个节点
}
ListNode* newNode = new ListNode(val); // 创建新节点
newNode->next = current->next;
current->next = newNode;
size++; // 增加链表大小
}
void deleteAtIndex(int index) {
if(index < 0 || index >= size) {
cout << "Index out of bounds, node not deleted." << endl;
return;
}
ListNode* current = head;
for(int i = 0; i < index; i++) {
current = current->next; // 遍历到指定索引的前一个节点
}
ListNode* p = current->next;
current->next = current->next->next;
delete p;
size--; // 减少链表大小
}
};
int main(){
// 测试代码
MyLinkedList* obj = new MyLinkedList();
obj->addAtHead(1);
obj->addAtTail(3);
obj->addAtIndex(1, 2);
cout << obj->get(1) << endl; // 应该输出2
obj->deleteAtIndex(1);
cout << obj->get(1) << endl; // 应该输出3
delete obj;
return 0;
}
4.反转链表
反转一个单链表。输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL。
首先要把 cur->next 节点用tmp指针保存一下,也就是保存这个节点。保存一下这个节点是因为接下来要改变 cur->next 的指向,将cur->next 指向pre ,此时已经反转了第一个节点。接下来,就是循环走如下代码逻辑,继续移动pre和cur指针。最后,cur指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以,pre指针就指向了新的头结点。
#include<iostream>
using namespace std;
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
// 反转链表
ListNode* reverseList(ListNode* head) {
ListNode* prev=NULL; // 前一个节点初始化为NULL
ListNode* current=head; // 当前节点指向头节点
while(current){
ListNode* temp=current->next; // 保存当前节点的下一个节点
current->next=prev; // 将当前节点的next指向前一个节点
prev=current; // 前一个节点更新为当前节点
current=temp; // 当前节点更新为下一个节点
}
return prev; // 返回新的头节点,即原链表的尾节点
}
int main(){
ListNode *head = new ListNode(1);
ListNode *second = new ListNode(2);
ListNode *third = new ListNode(3);
ListNode *fourth = new ListNode(4);
ListNode *fifth = new ListNode(5);
head->next = second;
second->next = third;
third->next = fourth;
fourth->next = fifth;
cout << "Original List: ";
ListNode* current = head;
while(current) {
cout << current->val << " ";
current = current->next;
}
cout << endl;
ListNode* reversedHead = reverseList(head); // 调用反转函数
cout << "Reversed List: ";
current = reversedHead;
while(current) {
cout << current->val << " "; // 输出反转后的链表
current = current->next;
}
return 0; // 程序结束
}
这道题还有一个递归的解法。
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) {
return head;
}
ListNode* newHead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
递归基例
if (!head || !head->next) {
return head;
}
- 如果链表为空(
!head
)或只有一个节点(!head->next
),直接返回当前头节点,因为无需反转。
递归步骤
ListNode* newHead = reverseList(head->next);
- 递归调用,反转以
head->next
开头的子链表,并返回新头节点newHead
。 - 假设递归调用正确反转了子链表,此时
head->next
已经是子链表的尾节点。
反转当前节点
head->next->next = head;
head->next = nullptr;
head->next->next = head
:将当前节点head
接在已反转子链表的尾部。head->next = nullptr
:断开当前节点与原下一个节点的连接,避免循环。
返回新头节点
return newHead;
- 在整个递归过程中,
newHead
始终是最初链表的尾节点,即反转后的新头节点,因此直接返回。
5.两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
保存两个临时节点temp1=current->next,temp2=current->next->next->next。 然后分别执行步骤一current->next=current->next->next,步骤二current->next->next=temp,步骤三current->next->next->next=temp2。
#include<iostream>
using namespace std;
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
ListNode* change(ListNode* head){
ListNode* dummy=new ListNode(0); // 创建一个虚拟头节点
dummy->next = head; // 将虚拟头节点的next指向原链表的头节点
ListNode* current = dummy; // 当前节点指针初始化为虚拟头节点
while(current->next !=nullptr&¤t->next->next !=nullptr){
ListNode* temp=current->next;
ListNode* temp1=current->next->next->next;
current->next = current->next->next; // 步骤一
current->next->next = temp; // 步骤二
current->next->next->next = temp1; // 步骤三
current = current->next->next; // cur移动两位,准备下一轮交换
}
ListNode* result = dummy->next;
delete dummy;
return result;
}
int main(){
ListNode *head = new ListNode(1);
ListNode *second = new ListNode(2);
ListNode *third = new ListNode(3);
ListNode *fourth = new ListNode(4);
ListNode *fifth = new ListNode(5);
head->next = second;
second->next = third;
third->next = fourth;
fourth->next = fifth;
ListNode* changehead=change(head); // 调用change函数
ListNode* current = changehead;
while(current) {
cout << current->val << " "; // 输出链表的值
current = current->next; // 移动到下一个节点
}
cout << endl; // 输出换行
return 0; // 程序结束
}
6.删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。进阶:你能尝试使用一趟扫描实现吗?
这是双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
定义fast指针和slow指针,初始值为虚拟头结点。 fast首先走n + 1步 ,只有这样同时移动的时候slow才能指向删除节点的上一个节点。 fast和slow同时移动,直到fast指向末尾。 删除slow指向的下一个节点 。
#include<iostream>
using namespace std;
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
ListNode* deleteN(ListNode* head,int n){
ListNode* dummp=new ListNode(0); // 创建一个虚拟头节点
dummp->next=head;
ListNode* fast=dummp; // 快指针
ListNode* slow=dummp; // 慢指针
for(int i=0;i<n;i++){
fast=fast->next; // 快指针先走n步
}
while(fast->next!=NULL){
fast=fast->next;
slow=slow->next;
}
slow->next=slow->next->next;
return dummp->next; // 返回新的头节点
}
int main(){
ListNode* head=new ListNode(1);
head->next=new ListNode(2);
head->next->next=new ListNode(3);
head->next->next->next=new ListNode(4);
head->next->next->next->next=new ListNode(5);
int n=2; // 删除倒数第n个节点
ListNode* newHead=deleteN(head,n);
ListNode* current=newHead;
while(current!=NULL){
cout << current->val << " "; // 输出新的链表
current=current->next;
}
cout << endl;
// 释放链表内存
current = newHead;
while (current != NULL) {
ListNode* temp = current;
current = current->next;
delete temp; // 删除当前节点
}
return 0; // 程序结束
}
7.链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null。
显而易见的只有当链表 headA 和 headB 都不为空时,两个链表才可能相交。因此首先判断链表 headA 和 headB 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null。
当链表 headA 和 headB 都不为空时,创建两个指针 pA 和 pB,初始时分别指向两个链表的头节点 headA 和 headB。
既然是相交节点,相交节点的后续部分一定完全一致,毕竟相交节点对于两个列表来说就是同一个节点,那么也就是说相交节点以及后续部分一定是位于列表最后部分的。所以只需要计算长列表和短列表的差值,即末端对齐,把长列表的pA移动到和pB一样的起点,之后同步向后移,只要有相交节点那么必然能同时相遇,返回即可,如果都是null那就返回null。
上下两个是不同的例子。
#include<iostream>
using namespace std;
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
ListNode* meet(ListNode* head1, ListNode* head2){
if(head1 == NULL || head2 == NULL) return NULL; // 如果任一链表为空,返回NULL
ListNode* p1=head1;
int size1; // 指向链表1的指针
ListNode* p2=head2;
int size2; // 指向链表2的指针
while(p1 != NULL){ // 计算链表1的长度
size1++;
p1 = p1->next;
}
while(p2 != NULL){ // 计算链表2的长度
size2++;
p2 = p2->next;
}
p1=head1; // 重置p1指向链表1的头节点
p2=head2; // 重置p2指向链表2的头节点
if(size1 > size2){ // 如果链表1更长
for(int i=0; i<size1-size2; i++){ // 让p1先走size1-size2步
p1 = p1->next;
}
}else{ // 如果链表2更长或两者相等
for(int i=0; i<size2-size1; i++){ // 让p2先走size2-size1步
p2 = p2->next;
}
}
while(p1 != NULL && p2 != NULL){ // 同时遍历两个链表
if(p1 == p2) return p1; // 如果相遇,返回相遇的节点
p1 = p1->next; // p1向后移动
p2 = p2->next; // p2向后移动
}
return NULL; // 如果没有相遇,返回NULL
}
int main(){
ListNode* head1 = new ListNode(1);
head1->next = new ListNode(2);
head1->next->next = new ListNode(3);
ListNode* head2 = new ListNode(4);
head2->next = new ListNode(5);
head2->next->next = head1->next; // 让head2与head1相交
ListNode* meetNode = meet(head1, head2); // 查找相遇节点
if(meetNode != NULL){
cout << "Meet at node with value: " << meetNode->val << endl; // 输出相遇节点的值
}else{
cout << "No meeting point." << endl; // 如果没有相遇,输出提示信息
}
// 释放链表内存
delete head1->next->next; // 删除链表1的第三个节点
delete head1->next; // 删除链表1的第二个节点
delete head1; // 删除链表1的头节点
delete head2->next; // 删除链表2的第二个节点
delete head2; // 删除链表2的头节点
return 0; // 程序结束
}
8.环形链表
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。说明:不允许修改给定的链表。
判断是否有环可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。首先,fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇 ,为什么fast指针和slow指针一定会相遇呢?可以画一个环,可以认为是fast指针在任意一个节点开始追赶slow指针。fast指针速度更快,所以必然相遇。
接下来要找这个环的入口,假设从头结点到环形入口节点的节点数为x(4)。 环形入口节点到 fast指针与slow指针相遇节点节点数为y(1)。 从相遇节点再到环形入口节点节点数为 z(3)。 如图所示:
那么相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, z + y
为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 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指针了。
当 n为1的时候,公式就化解为 x = z
,
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
#include<iostream>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
// 检测是否有环,并返回相遇点
ListNode* detectCycle(ListNode* head) {
if (head == nullptr || head->next == nullptr) return nullptr;
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
return slow; // 相遇点
}
}
return nullptr; // 无环
}
// 找到环的起始节点
ListNode* findCircleStart(ListNode* head) {
ListNode* meet = detectCycle(head);
if (meet == nullptr) return nullptr; // 无环
ListNode* slow = head;
while (slow != meet) {
slow = slow->next;
meet = meet->next;
}
return slow; // 环的起始节点
}
ListNode* createCircleList(int n) {
if (n <= 0) return nullptr;
ListNode* head = new ListNode(1);
ListNode* current = head;
for (int i = 2; i <= n; ++i) {
current->next = new ListNode(i);
current = current->next;
}
current->next = head; // 形成环
return head;
}
int main() {
int n = 5;
ListNode* head = createCircleList(n);
ListNode* circleStart = findCircleStart(head);
if (circleStart != nullptr) {
cout << "Circle starts at node with value: " << circleStart->val << endl;
} else {
cout << "No circle found." << endl;
}
// 释放内存(需要先断开环)
if (n > 0) {
ListNode* current = head;
for (int i = 0; i < n; ++i) {
ListNode* temp = current;
current = current->next;
delete temp;
}
}
return 0;
}