目录
3.1.1. 实现框架
3.1.2.1. push_back()的实现问题和解决办法:
1.链表的概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
2.链表的分类
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
注意:上面的图都是逻辑图,实际是不会有这些箭头的;
在这里以无头单向非循环链表举例,我们画一下它的物理图
为了更好理解链表的实际存储,我们用编译器进行调试,查看每个结点的地址,看看是否符合上述物理图。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int node_val_type;
typedef struct single_link_list_node
{
struct single_link_list_node* next;
node_val_type val;
}SLNode;
void print_node_val(SLNode* phead)
{
SLNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
int main()
{
SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));
assert(node1);
node1->val = 1;
SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
assert(node2);
node2->val = 2;
SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
assert(node3);
node3->val = 3;
node1->next = node2;
node2->next = node3;
node3->next = NULL;
print_node_val(node1);
free(node1);
free(node2);
free(node3);
}
显然,经过对比是符合我们预期,即链表的实际存储是按照物理图的方式进行存储的;
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表: 结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
3.链表的实现
3.1.无头单向非循环链表
3.1.1. 实现框架
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int node_val_type;
typedef struct single_link_list_node
{
struct single_link_list_node* next;
node_val_type val;
}SLNode;
void print_node_val(SLNode* phead);
void push_back_node_val(SLNode** pphead,node_val_type val);
void push_front_node_val(SLNode** pphead,node_val_type val);
void pop_back_node_val(SLNode** pphead);
void pop_front_node_val(SLNode** pphead);
SLNode* find_special_val_node(SLNode* phead,node_val_type val);
void insert_node_val(SLNode** pphead,SLNode* pos,node_val_type val); //在pos位置前面插入
void insert_node_val_after(SLNode* pos, node_val_type val); //在pos位置后面插入
void erase_node_val(SLNode** pphead,SLNode* pos);
3.1.2.1. push_back()的实现问题和解决办法:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int node_val_type;
typedef struct single_link_list_node
{
struct single_link_list_node* next;
node_val_type val;
}SLNode;
void push_back(SLNode* phead, node_val_type val)
{
//1.创建一个new node
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
assert(newnode);
newnode->val = val;
newnode->next = NULL;
//2.分两种情况处理
//a.空表
if (phead == NULL)
{
phead = newnode;
}
//b.不是空表
else
{
//1.找尾
SLNode* tail = phead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void print_node_val(SLNode* phead)
{
SLNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
int main()
{
SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));
assert(node1);
node1->val = 1;
SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
assert(node2);
node2->val = 2;
SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
assert(node3);
node3->val = 3;
node1->next = node2;
node2->next = node3;
node3->next = NULL;
print_node_val(node1);
push_back(node1, 4);
push_back(node1, 5);
push_back(node1, 6);
print_node_val(node1);
}
结果:
1->2->3->NULL
1->2->3->4->5->6->NULL
结果符合预期;但是有些情况我们是不是要在空表的基础上push_back(),这种场景是存在的,那么我们的push_back()能满足这种情况吗?
int main()
{
SLNode* phead = NULL;
push_back(phead, 1);
push_back(phead, 2);
push_back(phead, 3);
print_node_val(phead);
}
结果:
NULL
结果与预期不符合,奇怪了,怎么回事呢?为什么当是一个空表的时候,数据没有插入进去呢?遇到问题,我们去调试,
当我在函数内部调试时,发现数据的确被插入进去了呀,OK,不急我们继续往下看,
我们惊奇的发现,出了push_back()这个函数作用域,我们的phead竟然又变成NULL了,里面的数据不见了也情有可原;那为什么呢?OK要理解这个问题的原因,我们先看下面的代码:
void Func(double x)
{
x = 10;
}
int main()
{
double d = 5;
printf("old d = %lf\n", d);
Func(d);
printf("new d = %lf\n", d);
}
结果:
old d = 5
new d = 5
这个我们应该能理解,在Func1里面被改变的x是d的拷贝,在这个函数内部的改变无法影响到实参,如果我们要改变,需要传递d的地址,即我们要改变double的值,需要传递double*,即double类型的地址,那么我们要改变SLNode*,那么我们要传递SLNode**,即这个节点的地址;因此上面的push_back()之所以数据没有正常插入,是因为它是值传递,因此我们解决的方案是址传递;
//要改变 phead,需要传递 &phead,即形参需要用二级指针接受
void push_back_node_val(SLNode** pphead,node_val_type val)
{
assert(pphead); #pphead是(*pphead)的地址,正常情况下pphead不可能为NULL
//1.创建一个新节点
SLNode* newnode = build_new_node(val);
//2.插入数据,两种情况
//a.空表
if(*pphead == NULL)
{
*pphead = newnode;
}
//b.不是空表
else
{
SLNode* tail = *pphead;
while(tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void Test(void)
{
SLNode* phead = NULL;
push_back_node_val(&phead,1);
}
3.1.2.2. push_front()的实现
void push_front_node_val(SLNode** pphead,node_val_type val)
{
assert(pphead);
//1.创建一个new node
SLNode* newnode = build_new_node(val);
//2.插入数据
newnode->next = *pphead;
*pphead = newnode;
}
3.1.2.3. pop_back()的实现
void pop_back_node_val(SLNode** pphead)
{
assert(pphead);
//1.空表不能删
assert(*pphead);
//2.删除非空表,两种情况
//a.只有一个数据
if((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//b.2个或2个以上的数据
else
{
//遍历得到tail的前一个node
SLNode* tail_prev = *pphead;
while(tail_prev->next->next != NULL)
{
tail_prev = tail_prev->next;
}
SLNode* tail = tail_prev->next;
free(tail);
tail = NULL;
//3.最关键的一步,需要把删除后的最后一个数据的next域置为NULL(防止野指针)
tail_prev->next = NULL;
}
}
3.1.2.4.pop_front()的实现
void pop_front_node_val(SLNode** pphead)
{
assert(pphead);
//1.空表不能删
assert(*pphead);
//2.删除数据
SLNode* head_next = (*pphead)->next;
free(*pphead);
*pphead = head_next;
}
3.1.2.5. find()的实现
SLNode* find_special_val_node(SLNode* phead,node_val_type val)
{
SLNode* cur = phead;
while(cur != NULL)
{
if(cur->val == val)
return cur; # 返回目标节点
cur = cur->next;
}
return NULL; # 代表没找到
}
3.1.2.6. insert()的实现
//在pos位置之前插入
void insert_node_val(SLNode** pphead,SLNode* pos,node_val_type val)
{
assert(pphead);
//1.pos位置需要有效
assert(pos);
//2.创建一个new node
SLNode* newnode = build_new_node(val);
//插入数据,两种情况
//a.头插
if(pos == *pphead)
{
newnode->next = *pphead;
*pphead = newnode;
}
//非头插
else
{
SLNode* pos_prev = *pphead;
while(pos_prev->next != pos)
{
pos_prev = pos_prev->next;
}
pos_prev->next = newnode;
newnode->next = pos;
}
}
//在pos位置之后插入
void insert_node_val_after(SLNode* pos, node_val_type val)
{
assert(pos);
SLNode* newnode = build_new_node(val);
newnode->next = pos->next;
pos->next = newnode;
}
3.1.2.7. erase()的实现
//删除pos位置
void erase_node_val(SLNode** pphead,SLNode* pos)
{
assert(pphead);
assert(pos);
//1.头删
if(pos == *pphead)
{
//a.只有一个节点
if((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//b.多个节点
else
{
SLNode* head_next = (*pphead)->next;
free(*pphead);
*pphead = head_next;
}
}
else
{
SLNode* pos_prev = *pphead;
while(pos_prev->next != pos)
{
pos_prev = pos_prev->next;
}
SLNode* pos_next = pos->next;
free(pos);
pos_prev->next = pos_next;
}
}
//删除pos的下一个位置
void erase_node_val_after(SLNode* pos)
{
assert(pos && pos->next != NULL); #pos不为空 && pos不能为尾节点
SLNode* del = pos->next;
SLNode* del_next = del->next;
free(del);
del = NULL;
pos->next = del_next;
}
3.1.2.8. print()的实现
void print_node_val(SLNode* phead)
{
//assert(phead); #这里是不能断言的,空表是可以打印的
SLNode* cur = phead;
while(cur != NULL)
{
printf("%d->",cur->val);
cur = cur->next;
}
printf("NULL\n");
}
3.1.2.9. build()的实现
SLNode* build_new_node(node_val_type val) //创建一个值为val的新节点
{
SLNode* ret = (SLNode*)malloc(sizeof(SLNode));
assert(ret);
ret->val = val;
ret->next = NULL;
return ret;
}
3.1.2.10. destroy()的实现
void destroy(list_node** pphead)
{
list_node* cur = *pphead;
list_node* next = NULL;
while(cur != NULL)
{
next = cur->_next;
free(cur);
cur = next;
}
}
3.2.带头双向循环链表的实现
3.2.1. 实现框架
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int list_val_type;
typedef struct double_link_list
{
struct double_link_list* _prev;
struct double_link_list* _next;
list_val_type _val;
}DLlist,*PDlist;
//构造哨兵位的头结点
DLlist* build_head();
void print(DLlist* phead);
void push_back(DLlist* phead,list_val_type val);
void push_front(PDlist phead,list_val_type val);
void pop_back(DLlist* phead);
void pop_front(DLlist* phead);
void destroy(PDlist phead);
bool empty(PDlist phead);
PDlist find(PDlist phead,list_val_type val);
//在pos位置前插入新节点
void insert(PDlist pos,list_val_type val);
//删除pos所在的节点
void erase(PDlist pos);
3.2.2. 整体实现
#include "double_link_list.h"
# 构造哨兵位的头节点
DLlist* build_head()
{
DLlist* newnode = (DLlist*)malloc(sizeof(DLlist));
assert(newnode);
newnode->_prev = newnode->_next = newnode;
return newnode;
}
void print(DLlist* phead)
{
assert(phead);
DLlist* cur = phead->_next;
while(cur != phead->_prev)
{
printf("%d->",cur->_val);
cur = cur->_next;
}
if(cur != phead)
printf("%d\n",cur->_val);
}
# 构造一个有效节点
PDlist build_node(list_val_type val)
{
PDlist newnode = (PDlist)malloc(sizeof(DLlist));
assert(newnode);
newnode->_val = val;
newnode->_prev = newnode->_next = newnode;
return newnode;
}
bool empty(PDlist phead)
{
return phead->_next == phead;
}
PDlist find(DLlist* phead,list_val_type val)
{
//1. 从第一个有效节点开始
DLlist* cur = phead->_next;
while(cur != phead)
{
if(cur->_val == val)
return cur;
cur = cur->_next;
}
return NULL;
}
void push_back(PDlist phead, list_val_type val)
{
//1.哨兵位的头节点不可能为空
assert(phead);
//2.造新节点
PDlist newnode = build_node(val);
//3. 找尾
DLlist* tail = phead->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = phead;
phead->_prev = newnode;
}
void push_front(PDlist phead,list_val_type val)
{
//1.哨兵位的头结点不可能为空
assert(phead);
//2.造新节点
PDlist newnode = build_node(val);
assert(newnode);
//3.找到哨兵位的_next,在phead和phead->_next之间插入新节点
DLlist* head_next = phead->_next;
newnode->_next = head_next;
head_next->_prev = newnode;
newnode->_prev = phead;
phead->_next = newnode;
}
void pop_back(PDlist phead)
{
//1.哨兵位头节点不为空
assert(phead);
//2.空表不可删(不包含哨兵位的头节点)
assert(!empty(phead));
//3.找尾节点tail的_prev,将tail->_prev和phead连接起来,释放tail
DLlist* tail = phead->_prev;
DLlist* tail_prev = tail->_prev;
tail_prev->_next = phead;
phead->_prev = tail_prev;
free(tail);
}
void pop_front(DLlist* phead)
{
//1.哨兵位头节点不为空
assert(phead);
//2.空表不可删(不包含哨兵位的头节点)
assert(!empty(phead));
//3.找第一个有效节点(phead->_next)的_next,连接它和phead,释放第一个有效节点
DLlist* effective_head = phead->_next;
DLlist* effective_head_next = effective_head->_next;
phead->_next = effective_head_next;
effective_head_next->_prev = phead;
free(effective_head);
}
void insert(PDlist pos,list_val_type val)
{
//1. pos是不可能为NULL的
assert(pos);
//2.造新节点
DLlist* newnode = build_node(val);
//3.得到pos->_prev,将这个节点和newnode链接起来
DLlist* pos_prev = pos->_prev;
newnode->_next = pos;
pos->_prev = newnode;
newnode->_prev = pos_prev;
pos_prev->_next =newnode;
}
void erase(PDlist pos)
{
//1.既然是删除,链表一定是一个非空链表,即pos位置一定要合法
assert(pos);
//2.找到pos->_prev和pos->_next,连接着两个节点,释放pos即可
DLlist* pos_prev = pos->_prev;
DLlist* pos_next = pos->_next;
pos_prev->_next = pos_next;
pos_next->_prev = pos_prev;
free(pos);
}
void destroy(PDlist phead)
{
//1.哨兵位节点不可能为空
assert(phead);
//2. 从phead->_next开始(不包括phead),保存下一个节点,释放当前节点
DLlist* cur = phead->_next;
DLlist* next = NULL;
while(cur != phead)
{
next = cur->_next;
phead->_next = next;
next->_prev = phead;
free(cur);
cur = next;
}
free(head); // 将哨兵位的头节点也干掉
}
4.单链表的练习题
4.1. 第一题:移除链表元素
//方法一: 利用链表的删除
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* cur = head;
ListNode* prev = nullptr;
while(cur != nullptr)
{
if(cur->val == val)
{
//执行删除
//a. 头删
if(cur == head)
{
cur = cur->next;
delete head;
head = cur;
}
//b. !头删
else
{
ListNode* next = cur->next;
delete cur;
prev->next = next;
cur = next;
}
}
else
{
//遍历下一个元素
prev = cur;
cur = cur->next;
}
}
return head;
}
};
//方法二: 建立新表
class Solution {
public:
ListNode* build_node(int val)
{
ListNode* newnode = new ListNode;
newnode->next = nullptr;
newnode->val = val;
return newnode;
}
ListNode* removeElements(ListNode* head, int val) {
ListNode* cur = head;
ListNode* new_head = nullptr; //新链表
while(cur != nullptr)
{
//不需要删除的节点
if(cur->val != val)
{
//尾插到新链表里
//a. 空表
ListNode* newnode = build_node(cur->val);
if(new_head == nullptr)
{
new_head = newnode;
}
//b. !空表
else
{
// 找尾
ListNode* tail = new_head;
while(tail->next != nullptr)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//遍历下一个节点
cur = cur->next;
}
return new_head;
}
};
方法三: 同样是建立新表,但是不造新节点
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* cur = head;
ListNode* cur_next = nullptr;
ListNode* new_head = nullptr; //新建一个空链表
while(cur != nullptr)
{
// 尾插到新表中
if(cur->val != val)
{
cur_next = cur->next;
//a. 空表
if(new_head == nullptr)
{
new_head = cur;
new_head->next = nullptr; //在这里需要将尾节点的next置为nullptr
}
//b. !空表
else
{
//找尾
ListNode* tail = new_head;
while(tail->next != nullptr)
tail = tail->next;
tail->next = cur;
cur->next = nullptr; //在这里需要将尾节点的next置为nullptr
}
cur = cur_next;
}
// 遍历下一个
else
cur = cur->next;
}
ListNode* tail = new_head;
// 非空表再去找tail
if(tail != nullptr)
{
while(tail->next != nullptr)
tail = tail->next;
tail->next = nullptr; //在这里需要将尾节点的next置为nullptr
}
return new_head;
}
};
4.2. 第二题:反转链表
struct ListNode* reverseList(struct ListNode* head){
struct ListNode* n1,*n2,*n3;
n1 = NULL;
n2 = head;
n3 = NULL;
//利用头插的方式进行反转链表
while(n2 != NULL)
{
n3 = n2->next;
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3)
n3 = n3->next;
}
return n1;
}
# 方案二:同样是利用头插,只不过这需要建立一个新链表
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* new_head = nullptr;
ListNode* cur = head;
while(cur != nullptr)
{
ListNode* next = cur->next;
// a. 空表
if(new_head == nullptr)
{
new_head = cur;
new_head->next = nullptr;
cur = next;
}
// b. !空表
else
{
cur->next = new_head;
new_head = cur;
cur = next;
}
}
return new_head;
}
};
4.3. 第三题:不提供头结点,删除pos
假设现在有一个单链表,没有提供头指针,请删除pos所在的节点,如图所示:
void special_erase(list_node* pos)
{
list_node* pos_next = pos->_next; # 找到pos的下一个节点
pos_next->_val = pos->_val; # 赋值
pos->_next = pos_next->_next; # 更新_next域
}
4.4. 第四题:链表的中间节点
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* fast = head,*slow = head;
//fast为nullptr || fast->next为nullptr 就结束了
//fast != nullptr && fast->next != nullptr 继续的条件
while(fast != nullptr && fast->next != nullptr)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
4.5. 第五题:合并两个有序链表
class Solution {
public:
bool get_min(ListNode* p1,ListNode* p2)
{
return p1->val < p2->val;
}
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(list1 == nullptr) return list2;
if(list2 == nullptr) return list1;
ListNode* head1 = list1;
ListNode* head2 = list2;
ListNode* new_head = nullptr;
ListNode* new_tail = nullptr;
while(head1 != nullptr && head2 != nullptr)
{
//a. 头插
if(new_head == nullptr)
{
if(get_min(head1,head2))
{
new_head = new_tail = head1;
head1 = head1->next;
}
else
{
new_head = new_tail = head2;
head2 = head2->next;
}
}
//b. !头插
else
{
if(get_min(head1,head2))
{
new_tail->next = head1;
new_tail = head1;
head1 = head1->next;
}
else
{
new_tail->next = head2;
new_tail = head2;
head2 = head2->next;
}
}
}
// 如果其中一个链表为空,就尾插另一个链表
if(head1 != nullptr)
new_tail->next = head1;
if(head2 != nullptr)
new_tail->next = head2;
return new_head;
}
};
# 方案二,建立一个带哨兵位的新链表
class Solution {
public:
bool get_min(ListNode* left_node,ListNode* right_node)
{
return left_node->val < right_node->val;
}
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(list1 == nullptr)
return list2;
if(list2 == nullptr)
return list1;
ListNode* cur1 = list1;
ListNode* cur2 = list2;
ListNode* new_head = nullptr;
ListNode* new_tail = nullptr;
new_head = new_tail = new ListNode;
new_head->next = nullptr;
while(cur1 && cur2)
{
// cur1->val < cur2->val
//尾插list1的节点
if(get_min(cur1,cur2))
{
new_tail->next = cur1;
new_tail = cur1;
cur1 = cur1->next;
}
// cur1->val >= cur2->val
//尾插list2的节点
else
{
new_tail->next = cur2;
new_tail = cur2;
cur2 = cur2->next;
}
}
// 其中一个不为nullptr,就尾插谁
if(cur1 != nullptr)
new_tail->next = cur1;
if(cur2 != nullptr)
new_tail->next = cur2;
return new_head->next;
}
};
4.6. 第六题:链表中倒数第k个节点
链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
ListNode* fast = pListHead,*slow = pListHead;
ListNode* cur = pListHead;
size_t num = 0;
while(cur != nullptr)
{
cur = cur->next;
num++;
}
// 如果k大于链表元素个数,返回nullptr
if(k > num)
return nullptr;
// 正常情况下,fast先走k步
while(k--)
{
fast = fast->next;
}
// 同时走,当fast == nullptr时,结束,返回slow
while(fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
4.7. 第七题:链表分割
class Partition {
public:
bool get_min(ListNode* cmp_node, int val)
{
return cmp_node->val < val;
}
ListNode* partition(ListNode* pHead, int x) {
//建立两个带哨兵位的链表:
// val >= x 的链表
ListNode* max_head = nullptr,*max_tail = nullptr;
// val < x 的链表
ListNode* min_head = nullptr,*min_tail = nullptr;
min_head = min_tail = new ListNode(0);
max_head = max_tail = new ListNode(0);
// 遍历原链表的节点cur
ListNode* cur = pHead;
while(cur != nullptr)
{
// cur->val < x
// 尾插到min链表中
if(get_min(cur,x))
{
min_tail->next = cur;
min_tail = cur;
}
// cur->val >= x
// 尾插到max链表中
else
{
max_tail->next = cur;
max_tail = cur;
}
cur = cur->next;
}
min_tail->next = max_head->next;
//防止成环,因为可能最后一个元素是小于val的
//那么此时max_tail的next就只想这个元素,构成了一个环
max_tail->next = nullptr;
//提前保存min_head->next
ListNode* obj_node = min_head->next;
delete min_head;
delete max_head;
return obj_node;
}
};
如果最后不将max_tail->next == nullptr,那么此时有可能成环,如下图所示:
4.8. 第八题:链表回文结构
链表的回文结构_牛客题霸_牛客网 (nowcoder.com)
# 方案一:利用栈的LIFO
class PalindromeList {
public:
bool chkPalindrome(ListNode* A) {
std::stack<ListNode*> st;
//1. 找中间节点
ListNode* mid = A;
ListNode* slow = A;
ListNode* fast = A;
while(fast != nullptr && fast->next != nullptr)
{
slow = slow->next;
fast = fast->next->next;
}
mid = slow;
//2. 得到原表的元素个数
ListNode* tmp = A;
size_t cnt = 0;
while(tmp != nullptr)
{
tmp = tmp->next;
cnt++;
}
//3. 入栈
//a. 奇数个元素,入中间节点
ListNode* cur = A;
if(cnt % 2 != 0)
{
while(cur != mid->next)
{
st.push(cur);
cur = cur->next;
}
}
else
//b. 偶数个元素,不入中间节点
{
while(cur != mid)
{
st.push(cur);
cur = cur->next;
}
}
//出栈
//a. 奇数个元素,先出一个节点,这个节点不用判断
//然后依次比较栈顶数据和链表剩余数据,相等就出
if(cnt % 2 != 0)
{
st.pop();
cur = cur->next;
while(!st.empty() && st.top()->val == cur->val)
{
st.pop();
cur = cur->next;
}
}
//b. 偶数个元素
//依次比较栈顶数据和链表剩余数据,相等就出
else
{
while(!st.empty() && st.top()->val == cur->val)
{
st.pop();
cur = cur->next;
}
}
// 栈为nullptr,就是回文结构
return st.empty();
}
};
// 方案二:利用逆置的思想
class PalindromeList {
public:
bool chkPalindrome(ListNode* A) {
//1. 找到中间节点
ListNode* mid = nullptr;
ListNode* fast = A;
ListNode* slow = A;
while(fast != nullptr && fast->next != nullptr)
{
slow = slow->next;
fast = fast->next->next;
}
mid = slow;
//2. 逆置
//建立新表
ListNode* cur = mid;
ListNode* cur_next = nullptr;
ListNode* new_head = nullptr;
while(cur != nullptr)
{
// 利用头插
cur_next = cur->next;
cur->next = new_head;
new_head = cur;
cur = cur_next;
}
//3. 比较
ListNode* cmp_old_head = A;
ListNode* cmp_new_head = new_head;
// 有一个结束,就都结束了
while(cmp_old_head && cmp_new_head)
{
// 只要一个不相等,就返回false
if(cmp_new_head->val != cmp_old_head->val)
return false;
cmp_new_head = cmp_new_head->next;
cmp_old_head = cmp_old_head->next;
}
return true;
}
};
4.9. 第九题:相交链表
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* cur1 = headA;
ListNode* cur2 = headB;
int cnt1 = 1;
int cnt2 = 1;
// 得到listA的长度cnt1
while(cur1->next != nullptr)
{
cur1 = cur1->next;
++cnt1;
}
// 得到listB的长度cnt2
while(cur2->next != nullptr)
{
cur2 = cur2->next;
++cnt2;
}
// 如果相交,两个链表的尾节点应该一样
if(cur2 != cur1)
return nullptr;
// 如果走到这,说明这两个链表一定会有交点
// 默认listA是long_list,listB是short_list
// len为两个链表的长度差
int len = cnt1 - cnt2;
ListNode* long_list = headA;
ListNode* short_list = headB;
// 如果len < 0,更新对应关系
if(len < 0)
{
len *= -1;
long_list = headB;
short_list = headA;
}
// 长链表先走len步
while(len--)
{
long_list = long_list->next;
}
// 然后两个链表一起走,如果相等就为相交节点
while(long_list != short_list)
{
long_list = long_list->next;
short_list = short_list->next;
}
return long_list;
}
};
4.10. 第十题:环形链表
class Solution {
public:
bool hasCycle(ListNode *head) {
//利用快慢指针
ListNode* slow = head;
ListNode* fast = head;
// 如果带环,slow和fast一定会相遇,即带环
// 当slow进环之后,fast开始追击slow,每追击一次,它们的差值会-1
// 因为fast走两步,slow走一步,即等于fast走一步,slow不动,因次它们一定会相遇
// 如果fast和slow走的步数的差值为1,在有环的情况下,一定会相遇。
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast)
return true;
}
return false;
}
};
4.11. 第十一题:环形链表Ⅱ:
4.11.1. 第一种方法:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
// 如果有环,一定会相遇
if(slow == fast)
{
// 一个从相遇点走,一个从起始点走,相遇的节点就是入环的第一个节点
ListNode* meet = slow;
ListNode* start = head;
while(meet != start)
{
meet = meet->next;
start = start->next;
}
return meet;
}
}
//如果没有环,就没有相遇
return nullptr;
}
};
4.11.2. 第二种方法:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
//1. 再有环的前提下,找到meet,slow和fast的相交节点
ListNode* slow = head;
ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(fast == slow)
{
//得到meet
ListNode* meet = slow;
//2. 断开原链表,将问题转化为两个链表相交的问题
ListNode* new_head = meet->next;
meet->next = nullptr;
//3.解决链表相交,相交点即为入环的第一个节点
ListNode* tmp1 = head;
ListNode* tmp2 = new_head;
int len1 = 0; //tmp1的长度
int len2 = 0; //tmp2的长度
while(tmp1 != nullptr)
{
tmp1 = tmp1->next;
++len1;
}
while(tmp2 != nullptr)
{
tmp2 = tmp2->next;
++len2;
}
// 默认old_head是long_list,new_old是short_list
ListNode* old_head = head;
int cnt = len1 - len2;
ListNode* long_list = old_head;
ListNode* short_list = new_head;
// 如果cnt<0,需要更新对应关系
if(cnt < 0)
{
cnt *= -1;
long_list = new_head;
short_list = old_head;
}
// 长链表先走cnt步
while(cnt--)
{
long_list = long_list->next;
}
while(long_list != short_list)
{
long_list = long_list->next;
short_list = short_list->next;
}
return long_list;
}
}
//如果没环,返回nullptr
return nullptr;
}
};
4.12. 第十二题:复制带随即指针的链表
138. 复制带随机指针的链表 - 力扣(LeetCode)
第一步:
第二步:
第三步:
4.12.1. 代码实现:
class Solution {
public:
Node* build_node(int val)
{
Node* newnode = new Node(val);
newnode->next = newnode->random = nullptr;
return newnode;
}
Node* copyRandomList(Node* head) {
if(head == nullptr)
return nullptr;
//1. 遍历原表,复制原表节点,尾插到原节点的后面
Node* cur = head;
Node* newnode;
while(cur != nullptr)
{
newnode = build_node(cur->val);
newnode->next = cur->next;
cur->next = newnode;
cur = cur->next->next;
}
//2. 根据原节点,处理random
//a. 如果原表中节点的random为nullptr,那么新表中的节点的random也为nullptr
//b. 根据原节点位置,得到我们需要的random
cur = head;
Node* deal_node = cur->next;
while(cur != nullptr)
{
if(cur->random == nullptr)
{
deal_node->random = nullptr;
}
else
{
deal_node->random = cur->random->next;
}
cur = deal_node->next;
if(cur != nullptr)
deal_node = cur->next;
}
// 3. 建立新表,恢复原表
Node* newhead = nullptr;
Node* newtail = nullptr;
cur = head;
//需要被摘除的节点
Node* remove_node = cur->next;
while(cur != nullptr)
{
//恢复原表
cur->next = remove_node->next;
//处理空表
if(newhead == nullptr)
{
newhead = newtail = remove_node;
newtail->next = nullptr;
}
//处理!空表
else
{
newtail->next = remove_node;
newtail = remove_node;
newtail->next = nullptr;
}
cur = cur->next;
if(cur != nullptr)
remove_node = cur->next;
}
return newhead;
}
};
5. 链表和顺序表的区别:
·注意:我们在这里说的链表说的是带头双向循环链表
不同点 | 顺序表 | 链表 |
存储空间上 |
物理上一定连续
|
逻辑上连续,但物理上不一定连续
|
随机访问
| 支持 |
不支持
|
任意位置插入或者删除元素
| 可能需要移动元素,效率较低,O(N) | 只需要修改指针的指向,O(1) |
插入 |
动态顺序表,空间不够时需要扩容(也存在空间浪费的问题)
| 按需申请内存 |
应用场景 |
元素高效存储
+
频繁访问
|
任意位置插入和删除频繁
|
缓存利用率 | 高 | 低 |
如何理解缓存利用率:
在这里有一个问题:既然内存比磁盘快,为什么还需要磁盘?
a. 内存是带电存储,必须通电才能存储数据
b. 内存的单位造价是比磁盘高很多的,而对于普通大众来说,对于性能的要求不是太高,更多的关注的是性价比。
假设我现在要遍历上面的vector和list,而这些数据结构的数据是存储于内存中的,当CPU处理这些数据的时候,是不会直接先去内存中拿数据的(因为CPU也认为内存太慢了),而是CPU先去缓存中找这些所需要的数据,如果找到了,就从缓存里面访问(命中),如果没找到(不命中),就会去内存中将数据加载到缓存,而CPU在加载数据的时候,不会只加载其中一个数据,而是会加载一段数据,例如上面的例子,假如我现在要遍历vector中的1-5的数据,第一次我访问1这个元素的时候,可能不命中,这时候就会将1后面的一段数据全部加载到缓存中(具体为多少,跟CPU有关,我们在这里就简单的认为全部加载到缓存中),那之后我们访问剩下的数据时,就会全部命中(缓存利用率高)。
但是如果是list,由于list只在逻辑上连续,而物理空间是不一定连续的,因此当遍历时,第一个数据不命中后,CPU就会去内存将该数据后的一段数据加载到缓存中,这些加载的空间很有可能就不是我们需要的元素(即不命中,缓存利用率低),同时,这时候也会带来另一个问题,缓存污染。