链表
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; // 返回真实头结点
}
};