文章目录
单链表–老师讲的
单链表
特点分析
-
若有尾结点, 尾插尾删是O(1)
-
在 单链表 设计中,头结点(Head Node)通常不是有效元素,而是一个 哨兵节点(dummy node),主要用于简化链表操作,比如插入、删除等。
-
分布式系统的CAP定理: 在 分布式系统 中,无法同时满足以下三个特性
C: 一致性:所有节点在同一时间看到的数据是一致的(强一致性)。
A: 可用性:系统始终能响应请求,不会返回错误或超时。
P: 分区合理性 :即使网络分区(节点间通信中断),系统仍能继续运行。由于网络分区(P)在分布式系统中不可避免(如网络故障、机器宕机),实际选择通常是 CP 或 AP
为什么无法同时满足 CAP?
- 网络分区(P)不可避免:分布式系统中,节点间通信可能失败(网络故障、机器宕机)。
- C 和 A 冲突:
- 如果要保证 一致性(C),在分区发生时必须阻塞写入或读取(牺牲可用性)。
- 如果要保证 可用性(A),在分区发生时必须允许不一致的读写(牺牲一致性)。
-
计算机 里 很少有最好的, 只有适合的
-
搜索上, 有序地数组是 比 链表高的 – 二分
-
怎么选择数组和链表?
下标访问/随机访问多, 搜索多(有序), 用数组
增删多, 用链表
-
单链表接口实现:
自己实现链表: 写 链表接口-- 头插, 尾插, 删除一个节点, 删除节点(有重复), 搜索, 打印
// 单链表代码实现 // 节点类型---可以放进类里,详见单向循环链表 struct Node { Node(int data = 0) :data_(data), next_(nullptr) {} int data_; Node* next_; }; class Clink { public: Clink() { // 给head_初始化指向头节点 head_ = new Node(); } ~Clink() { // 节点的释放 Node* p = head_; while (p != nullptr) { head_ = head_->next_; delete p; p = head_; } head_ = nullptr; } public: // 链表尾插法 O(n) head_:头节点 tail_:尾节点 void InsertTail(int val) { // 先找到当前链表的末尾节点 Node* p = head_; while (p->next_ != nullptr) { p = p->next_; } // 生成新节点 Node* node = new Node(val); // 把新节点挂在尾节点的后面 p->next_ = node; } // 链表的头插法 O(1) void InsertHead(int val) { Node* node = new Node(val); node->next_ = head_->next_; head_->next_ = node; } // 链表节点的删除 void Remove(int val) { Node* q = head_; Node* p = head_->next_; while (p != nullptr) { if (p->data_ == val) { // 删除一个节点本身的操作是O(1) q->next_ = p->next_; delete p; return; } else { q = p; p = p->next_; } } } // 删除多个节点 void RemoveAll(int val) { Node* q = head_; Node* p = head_->next_; while (p != nullptr) { if (p->data_ == val) { q->next_ = p->next_; delete p; // 对指针p进行重置 p = q->next_; } else { q = p; p = p->next_; } } } // 搜索 list O(n) 数组的搜索 下表访问/随机访问arr[i]O(1) 搜索O(n) bool Find(int val) { Node* p = head_->next_; while (p != nullptr) { if (p->data_ == val) { return true; } else { p = p->next_; } } return false; } // 链表打印 void Show() { Node* p = head_->next_; while (p != nullptr) { cout << p->data_ << " "; p = p->next_; } cout << endl; } private: Node* head_; // 指向链表的头节点 };
单链表-复习
new Node()
和 new Node
的区别
表达式 | 行为 |
---|---|
new Node() | 调用 Node 的构造函数,并进行值初始化(data_ 会被初始化为 0 ,next_ 初始化为 nullptr )。 |
new Node | 调用 Node 的构造函数,但不进行值初始化(如果 Node 是 POD 类型,可能会有未定义值,但这里 Node 有构造函数,所以行为一致)。 |
POD = 像 C 结构体一样简单的数据类型。
何为POD
-
定义
- 简单的数据类型,兼容 C 语言(如
int
、float
、不含虚函数的struct
)。 - 标准布局(成员排列紧凑) + 平凡构造/析构(无自定义构造/拷贝/析构)。
- 简单的数据类型,兼容 C 语言(如
-
特点
-
可直接
memcpy
复制、二进制存储(如网络传输)。 -
示例:
cpp
复制
struct Point { int x; int y; }; // POD
-
-
非 POD 例子
- 含虚函数、自定义构造函数、
std::string
等(不可直接内存操作)。
- 含虚函数、自定义构造函数、
-
用途
- 底层操作(序列化、C 交互)时需保证类型是 POD。
自己练习:
自己实现链表: 写 链表接口-- 头插, 尾插, 删除一个节点, 删除节点(有重复), 搜索, 打印
#include <iostream>
using namespace std;
// 先定义 节点
struct Node
{
Node(int data = 0)
:data_(data)
, next_(nullptr)
{ }
int data_;
Node* next_;
};
class Clink
{
public:
Clink()
{
head_ = new Node();
}
~Clink()
{
Node* p = head_;
while (p != nullptr)
{
head_ = head_->next_;
delete p;
p = head_;
}
head_ = nullptr; // 防止野指针
}
//头插
void InsertHead(int data)
{
Node* node = new Node(data);
node->next_ = head_->next_;
head_->next_ = node;
}
//尾插
void InsertTail(int data)
{
Node* p = head_;
while (p->next_ != nullptr)
{
p = p->next_;
}
// 此时p 就在 要插入的位置
Node* node = new Node(data);
p->next_ = node;
}
//删除一个----先要查找在不在
void Remove(int data)
{
Node* p = head_;
Node* q = head_->next_;
if (Find(data))
{
while (q != nullptr)
{
if (q->data_ == data)
{
p->next_ = q->next_;
delete q;
q = nullptr;
return;
}
else
{
p = p->next_;
q = q->next_;
}
}
}
}
//有重复值的 删除
void RemoveAll(int data)
{
Node* p = head_;
Node* q = head_->next_;
if (Find(data))
{
while (q != nullptr)
{
if (q->data_ == data)
{
p->next_ = q->next_;
delete q;
q = p->next_;
}
else
{
p = p->next_;
q = q->next_;
}
}
}
}
//查找
bool Find(int data)
{
Node* p = head_;
while (p != nullptr)
{
if (p->data_ == data)
{
return true;
}
else
{
p = p->next_;
}
}
return false;
}
//打印
void show()
{
Node* p = head_->next_;
while (p != nullptr)
{
cout << p->data_ << " ";
p = p->next_;
}
cout << endl;
}
private:
Node* head_;
};
int main()
{
Clink clink;
clink.InsertHead(1);
clink.InsertHead(2);
clink.InsertHead(3);
clink.InsertHead(4);
clink.show(); // 4 3 2 1
clink.InsertTail(5);
clink.show(); // 4 3 2 1 5
clink.Remove(4);
clink.show(); //3 2 1 5
clink.InsertTail(2);
clink.show(); // 3 2 1 5 2
clink.RemoveAll(2);
clink.show(); // 3 1 5
}
笔试面试
单链表逆序–后面的无法往前走!! 双指针就不行了
单链表逆序—头插
一般单链表第一个head节点, 不是一个有效的节点—使用这个特性
//核心
q=p->next;
p->next=head->next;
head->next=p;
p=q;
// 单链表逆序---可以从第二个开始, 节省了第一个插入
void ReverseLink(Clink &link)
{
Node* head = link.head_;
Node* p = head->next_;
if (p == nullptr)
{
return;
}
head->next_ = nullptr;
while (p != nullptr)
{
Node* q = p->next_;
// p指针指向的节点进行头插
p->next_ = head->next_;
head->next_ = p;
p = q;
}
}
单链表倒数第k个节点—一定要多画图, 一定不要让程序挂掉
首先并不知道 链表有多少节点, 那么这个 倒数第k个 就无法一个指针 直接从头走了
而 如果 先逆序, 再正着找, 步骤多
使用数组, 更是 复杂度提高了
本题还是双指针(快慢指针), 两个指针直接 差着 k-1个节点, 这样当快指针到达尾部, 慢的指针指向就是 这个 倒数第k个节点
// 求倒数第k个节点的值
bool GetLaskKNode(Clink& link, int k, int& val)//有成功又失败,一般是返回bool, 使用引用带回结果
{
Node* head = link.head_;
Node* pre = head;
Node* p = head;
if (k < 1)//不判断,程序会有机会崩溃, 访问了 空指针
{
return false;
}
for (int i = 0; i < k; i++)
{
p = p->next_;
if (p == nullptr) // 防止k>元素数量
{
return false;
}
}
// pre在头节点,p在正数第k个节点
while (p != nullptr)
{
pre = pre->next_;
p = p->next_;
}
val = pre->data_;
return true;
}
合并两个有序地链表–双指针应用在两个链表上
实际是3指针, 两个用于遍历, 一个用于插入
// 合并两个有序的单链表
void MergeLink(Clink& link1, Clink& link2)
{
Node* p = link1.head_->next_;
Node* q = link2.head_->next_;
Node* last = link1.head_;
link2.head_->next_ = nullptr;
while (p != nullptr && q != nullptr)
{
if (p->data_ < q->data_)
{
last->next_ = p;
p = p->next_;
last = last->next_;
}
else
{
last->next_ = q;
q = q->next_;
last = last->next_;
}
}
if (p != nullptr)
{
last->next_ = p;
}
else
{
last->next_ = q;
}
}
判断单链表是否有环? 求环 的 入口节点–超经典问题
环: 即找不到末尾阶段了, 没有末尾节点是 nullptr
-
方法1:主要是 识别 链表地址 重不重复,使用一个数组 记录 节点的地址—不建议开辟额外的内存
-
尽量原地解决! 双指针的应用, 快慢指针, 环, 能套一圈, 就是有环了, 慢指针一次跑一个, 快指针一次跑两个, 快的先跑到空, 就是没环, 当二者能遇上, 那么就有环了
-
b的挖坑: 快指针跑的快,刚好把慢的越过去了, 会不会每次 都会跳过了?(会越过去的前提,是快慢已经相等了, 才会跨过)
-
那使用b的方法, 怎么找 环的入口和环的入口的前一个呢?
使用数学推导: 结论: 在再次相遇的地方, 定义一个指针,开头处再定义一个,两个一块走(a=x,这个a实际比图中的多了一步,注意分析), 相遇的地方, 就是 环入口
// 判断单链表是否存在环,存在返回环的入口节点
bool IsLinkHasCircle(Node *head, int& val)
{
Node *fast = head;
Node* slow = head;
while (fast != nullptr && fast->next_ != nullptr)//两步都得判断
{
slow = slow->next_;
fast = fast->next_->next_;
if (slow == fast)
{
// 快慢指针再次遇见,链表存在环
fast = head; // 开始找入口, 重用指针
while (fast != slow)
{
slow = slow->next_;
fast = fast->next_;
}
val = slow->data_;
return true;
}
}
return false;
}
判断两个单链表是否相交, 返回相交节点的值
相交的定义:两个单链表从某个节点开始,它们之后的节点完全相同,即从某个节点开始,两个链表共享相同的后续节点。
传统解法: 用数组 存一个链表地址, 找重复的第二个链表
**原地解法: **双指针—原地解法通过计算两个链表的长度差,让长链表的指针先走一段距离,再同步遍历,直到找到相交节点或返回 nullptr
。
// 判断两个单链表是否相交,如果相交,返回相交节点的值
bool IsLinkHasMerge(Node* head1, Node* head2, int& val)
{
int cnt1 = 0, cnt2 = 0;
Node* p = head1->next_;
Node* q = head2->next_;
while (p != nullptr)
{
cnt1++;
p = p->next_;
}
while (q != nullptr)
{
cnt2++;
q = q->next_;
}
p = head1;
q = head2;
if (cnt1 > cnt2)
{
// 第一个链表长
int offset = cnt1 - cnt2;
while (offset-- > 0)
{
p = p->next_;
}
}
else
{
// 第二个链表长
int offset = cnt2 - cnt1;
while (offset-- > 0)
{
q = q->next_;
}
}
while (p != nullptr && q != nullptr)
{
if (p == q)
{
val = p->data_;
return true;
}
p = p->next_;
q = q->next_;
}
return false;
}
力扣-19:删除倒数第n个
此题的坑点, 没有第一个哨兵节点, 可以加一个临时的哨兵, 使得逻辑更简单
struct ListNode *removeNthFromEnd(struct ListNode *head, int n)
{
// 在函数内部加一个哨兵节点, 问题就很简单, 思考的地方也会很少
struct ListNode head_; // 在栈区, 仅这个函数用
head_.next = head;
struct ListNode *p = &head_; // c里面访问结构体, struct不能省略
struct ListNode *q = &head_;
for (int i = 0; i < n; ++i) // 找要删除的前一个, 差值要正确
{
if (p->next == NULL)
return head;
p = p->next;
}
while (p->next != NULL)
{
p = p->next;
q = q->next;
}
struct ListNode *del = q->next;
q->next = q->next->next;
free(del);
return head_.next;
}
力扣-61:旋转链表
后面的依次使用头插—不能一个一个插, 单链表无法找到前一个
仔细观察, 就是把后面的整体做一个前插
//自己的
//这种会出错, k=size时,会导致移动到最后访问越界
//int m = k;
//if(size<k) m = k%size;
struct ListNode* rotateRight(struct ListNode* head, int k) {
struct ListNode *p = head;
struct ListNode *q = head;
int size = 1;
if (p==NULL || p->next == NULL)
{
return head;
}
while (p->next != NULL)
{
p = p->next;
++size;
}
p = head;
int m = k%size;
for (int i = 0; i < m; ++i)
{
p = p->next;
}
while (p->next != NULL)
{
p = p->next;
q = q->next;
}
p->next = head;
head = q->next;
q->next = NULL;
return head;
}
//老师的 空间复杂度O(1)
struct ListNode *rotateRight(struct ListNode *head, int k)
{
struct ListNode *p = head;
struct ListNode *q = head;
int number = 0;
if (p==NULL || k==0)
{
return head;
}
//O(n)
for(struct ListNode *m=head;m!=NULL;m=m->next)
{
++number;
}
k=k%number;
if(k==0) // 这里加一个, 避免如冗余
{
return head;
}
//O(n)
for(int i=0;i<k;++i)
{
p=p->next;
}
while (p->next != NULL)
{
p = p->next;
q = q->next;
}
p->next = head;
head = q->next;
q->next = NULL;
return head;
}