基础知识
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)
链表类型
- 单链表:指针域只能指向节点的下一个节点;
- 双链表:节点有两个指针域,一个指向下一个节点,一个指向上一个节点;
- 循环链表:就是链表首尾相连,可以用来解决约瑟夫环问题。
链表的存储方式
不同于数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
链表的定义
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
之所以要定义构造函数而不是使用默认构造函数的话,在初始化的时候就能直接给变量赋值!
链表操作
-
删除节点
-
添加节点
与数组的关系
- 数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
- 链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,=频繁增删,较少查询的场景。
题目
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) {
ListNode* analoghead = new ListNode();
analoghead->next = head;
ListNode* cur = analoghead;
while(cur->next != nullptr){
if(cur->next->val==val){
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
}
else{
cur = cur->next;
}
}
head = analoghead->next;
delete analoghead;
return head;
}
};
2.设计链表
题目链接
在一个列表类中实现以下功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
MyLinkedList() {
_size = 0;
head = new LinkedNode(0);
}
int get(int index) {
if(index > _size-1 || index < 0) return -1;
LinkedNode* cur = head->next;
while(index--){
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
LinkedNode* newhead = new LinkedNode(val);
newhead->next = head->next;
head->next = newhead;
_size++;
}
void addAtTail(int val) {
int nums = _size;
LinkedNode* newtail = new LinkedNode(val);
LinkedNode* cur = head;
while(nums--){
cur = cur->next;
}
cur->next = newtail;
_size++;
}
void addAtIndex(int index, int val) {
if(index>_size) return;
if(index<0) index = 0;
LinkedNode* newtail = new LinkedNode(val);
LinkedNode* cur = head;
while(index--){
cur = cur->next;
}
newtail->next = cur->next;
cur->next = newtail;
_size++;
}
void deleteAtIndex(int index) {
if(index > _size-1 || index < 0) return;
LinkedNode* cur = head;
while(index--){
cur = cur->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
private:
int _size;
LinkedNode* head;
};
};
3.反转链表
题目链接
给定一个链表,反转其排列顺序(反转链表节点的指针域)。
使用虚拟头结点:从链表起点开始,以k-1为cur结点;交换k(k=0,1,2,,,)的next指向,因此需要额外设置节点存储变量保存原本k节点的next,用于下一次遍历(cur起点是k)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* tmp = new ListNode();
ListNode* cur = head;
ListNode* pre = nullptr;
while(cur){
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
可以看出在反转链表时,至少需要3个变量分别存储pre,cur,tmp代表前一个节点,当前节点,下一个节点。
时间复杂度: O(n);空间复杂度: O(1)
class Solution {
public:
ListNode* reverse(ListNode* pre, ListNode* cur){
if(cur==nullptr) return pre;
ListNode* tmp = cur->next;
cur->next = pre;
return reverse(cur, tmp);
}
ListNode* reverseList(ListNode* head) {
return reverse(nullptr, head);
}
};
该题也有递归的解法,递归结束标志是cur==nullptr
时,在每层递归逻辑中将cur指向pre
,初始化条件时cur=head
,pre=nullptr
。
时间复杂度: O(n), 要递归处理链表的每个节点;空间复杂度: O(n), 递归调用了 n 层栈空间;
4.两两交换链表节点
题目链接
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表(不能只是修改节点的值,而是要进行实际的节点位置交换)。
使用虚拟头结点:从链表起点开始,以k-1为cur结点;交换k,k+1的位置,同时修改k,k+1的next指向,因此需要额外设置节点存储变量保存原本两个节点的next-tmp0,tmp1,用于下一次遍历(起点是k+1)
使用模拟头部节点,在步骤一前,需要存储节点1指针(cur的next指针);在步骤二前,需要存储节点3的指针(节点2的next指针);步骤三后,将cur绑定为节点1,进入下一步遍历。
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* analoghead = new ListNode();
analoghead->next = head;
ListNode* cur = analoghead;
while(cur->next!=nullptr && cur->next->next!=nullptr){
ListNode* tmp1 = cur->next;
ListNode* tmp2 = cur->next->next;
tmp1->next = tmp2->next;
tmp2->next = tmp1;
cur->next = tmp2;
cur = tmp1;
}
return analoghead->next;
}
};
5.删除链表倒数第N个结点
题目链接
使用虚拟结点-使用双指针(快指针,慢指针之间间隔设置为与N相关的量):L-p,R-p同时从虚拟结点开始,R-p先移动N+1位置,然后两指针同时右移,直到R-p指向nullptr,此时L-p所指向的结点为N-1,即可对其next指针域进行修改;
删除倒数第N个结点,需要在倒数第N+1个结点进行操作;因此快指针与慢指针之间因相差N+1个结点。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* analoghead = new ListNode();
analoghead->next = head;
ListNode* right_point = analoghead;
ListNode* left_point = analoghead;
while(n-- && right_point!=nullptr){
right_point = right_point->next;
}
while(right_point->next){ // 使用analoghead遍历结束标志
right_point = right_point->next;
left_point = left_point->next;
}
ListNode* tmp = left_point->next;
left_point->next = left_point->next->next;
delete tmp;
return analoghead->next;
}
};
时间复杂度: O(n);空间复杂度: O(1)
6.链表相交
题目链接
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
链表有交点意味了两链表的后部分的结点存在相同的指针,而链表有长短之分,因此可以将链表右端对齐,从较短链表开头位置设置指针逐步进行指针域比较。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int size1 = 0;
int size2 = 0;
ListNode* cur = headA;
while(cur){
size1++;
cur = cur->next;
}
cur = headB;
while(cur){
size2++;
cur = cur->next;
}
ListNode* pointa = new ListNode();
pointa->next = headA;
ListNode* pointb = new ListNode();
pointb->next = headB;
if(size1>size2){
int num = size1-size2;
while(num--) pointa = pointa->next;
}
else{
int num = size2-size1;
while(num--) pointb = pointb->next;
}
while(pointa->next!=nullptr){
if(pointa->next == pointb->next) return pointa->next;
pointa = pointa->next;
pointb = pointb->next;
}
return nullptr;
}
};
7.环形列表
题目链接
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。为了表示给定链表中的环,使用整数pos来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果pos 是-1,则在该链表中没有环。
使用双指针:slow,fast分别以1,2的速度从头结点出发,直至结点相遇。
2
(
x
+
y
)
=
x
+
y
+
n
(
y
+
z
)
x
=
(
n
−
1
)
(
y
+
z
)
+
z
x
=
z
\begin{align} 2(x+y) = x+y+n(y+z) \\ x = (n-1)(y+z)+z \\ x=z \end{align}
2(x+y)=x+y+n(y+z)x=(n−1)(y+z)+zx=z
根据slow与fast相遇节点位置,可以推导存在以下数学关系:x=z。那么从头节点和相遇节点分别同时出发一个指针,当这两个指针相遇的地方就是环形入口。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while(fast!=nullptr && fast->next!=nullptr){
slow = slow->next;
fast = fast->next->next;
if(slow==fast){
ListNode* index1 = head;
ListNode* index2 = slow;
while(index1!=index2){
index1 = index1->next;
index2 = index2->next;
}
return index1;
}
}
return nullptr;
}
};
时间复杂度: O ( n ) O(n) O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n;空间复杂度 O ( 1 ) O(1) O(1)
总结
链表涉及的知识&问题主要有:
- 设计链表
- 移除链表元素
- 反转链表
- 交换链表中两节点的数值
- 删除链表倒数第N个节点
- 判断两链表相交节点的位置
- 环形链表