文章目录
本部分知识比较简单。算法题比较基础,并且比较注重代码细节。注意:尽可能的一题多解。
基础知识简单总结
单向链表
- 由于比较简单,基本知识主要是CURD。(创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete))。
推荐参考
link1,link2。 - 下面模仿C++ STL完成一段简单的单向链表的实现(对节点进行了简单封装)。
(由于只是简单实现基本功能,很多部分并没有进行安全性和效率上考虑,测试不够完备。源码和当时实现部分注释附在下面。有兴趣的可以帮忙改进,感谢Thanks♪(・ω・)ノ)
#include <iostream>
using namespace std;
//定义节点
template <class T>
struct node
{
T data;
node* next;
node() : data(T()), next(nullptr) {}; //T 类型必须含有完成的赋值函数,防止其他错误。
node(T val) : data(val), next(nullptr) {};
node(T val, node* ptr) : data(val), next(ptr) {};
virtual ~node() {};
};
// 别名
using node_nums = unsigned long int;
//template <class T>
//using list_node = node<T>;
// 1.注释:
// 注意语法 typedef不能为模板类取别名
// 比如下语句无法通过编译:
/*
* template <class T>
* typedef struct node<T> list_node;
*/
// 2.如果强行使用,可以借助包裹一个类的方式实现
/*
* template <class T>
* struct list_node {
* typedef struct node<T> node;
* };
*/
//使用时候必须如下使用:
/* list_node<int>::node a;
* cout << a.data;
*/
// 3. 更方便的用法是借助using关键字,如上
// 为节点定义指针(迭代器)以及相关操作,方便使用
template<class T>
struct list_node_ptr {
node<T>* iter;
// 构造函数
list_node_ptr() : iter(nullptr) {};
list_node_ptr(T val) {
iter = new node<T>(val);
}
list_node_ptr(T val, list_node_ptr ptr) {
iter = new node<T>(val, ptr);
}
// 拷贝构造函数
list_node_ptr(const list_node_ptr& x) : iter(x.iter) {}
// 析构函数
virtual ~list_node_ptr() {};
// *iter
T& operator* () const {
return (*iter).data;
}
// 注意 -> 为一元操作运算符,返回指向自身元素的指针
node<T>* operator-> () const {
return &(operator*());
}
// 为了简单方便,重载++表示类似ptr = ptr-> next含义。
// 后置++ (ptr++)
list_node_ptr<T> operator++ (int) {
list_node_ptr<T> temp = *this; // 注意: 这里调用的是拷贝构造,不是*的重载。原因1.编译器首先遇到 “=”, 2. * 对本身元素,不是指向自己的指针
++*this;
return temp;
}
// 前置++ (++ptr)
list_node_ptr<T>& operator++() {
iter = (*iter).next;
return *this;
}
};
//定义单向链表和基本操作
template <class T>
class forward_linklist
{
private:
// 数据段
list_node_ptr<T> head_node; // 头元素指针(迭代器)
node_nums list_len; // 链表长度
// 保证只有一个实例,以防使用时候出现异常错误
forward_linklist() : head_node(), list_len(0) {};
virtual ~forward_linklist() {
forward_linklist_clear();
}
// 禁用拷贝
forward_linklist(const forward_linklist& ptr) = delete;
public:
// 两种初始化方式: 默认和列表初始化
static forward_linklist* get_linklist() {
return new forward_linklist();
}
static forward_linklist* get_linklist(const initializer_list<T>& lst) {
auto ptr = new forward_linklist();
for (const auto& val : lst) {
ptr->add_node(val, ptr->get_len());
}
return ptr;
}
// 查找操作:
list_node_ptr<T>& find_node(node_nums pos) {
auto ptr = head_node;
if (pos >= list_len) {
return list_node_ptr<T>();// 暂时使用空表示查找失败
}
for (int i = 1; i < pos; ++i) {
++ptr;
}
return ptr;
}
// 删除操作:
// 删除第i个元素(下标从0开始计数),返回删除的元素前一个元素指针(迭代器)
// 如果是第一个元素,和失败一样返回nullptr
list_node_ptr<T>& delete_node(node_nums pos) {
if (pos >= list_len) {
return list_node_ptr<T>();// 暂时使用空表示删除失败
}
else if (pos == 0) { // 表示删除第一个元素
auto temp = head_node++;
delete temp.iter;
--list_len;
return list_node_ptr<T>();
}
auto prior_ptr = find_node(pos - 1);
auto del_ptr = prior_ptr;
++del_ptr;
auto next_ptr = del_ptr;
++next_ptr;
delete del_ptr.iter;
prior_ptr.iter->next = next_ptr.iter;
--list_len;
return prior_ptr;
}
// 修改操作:
// 修改第pos个元素,值改为val,返回修改后的元素迭代器
list_node_ptr<T>& update_node(T val, node_nums pos) {
if (pos >= list_len) {
return list_node_ptr<T>(); // 暂时使用空表示修改失败
}
auto ptr = find_node(pos);
*ptr = val;
return ptr;
}
// 插入操作:在第pos个元素后面进行插入元素操作(默认尾端插入)
// 返回插入后的位置迭代器
list_node_ptr<T>& add_node(T val, node_nums pos = list_len) {
if (pos > list_len) {
return list_node_ptr<T>();
} else if (pos == list_len && pos == 0) { // 插入第一个元素
head_node = list_node_ptr<T>(val);
++list_len;
return head_node;
}
auto ptr = list_node_ptr<T>(val);
auto insert_node = head_node;
for (int i = 1; i < pos; ++i) {
++insert_node;
}
auto insert_next_node = insert_node;
++insert_next_node;
insert_node.iter->next = ptr.iter;
ptr.iter->next = insert_next_node.iter;
++list_len;
return ptr;
}
// 清空所有元素
void forward_linklist_clear() {
while (list_len) {
list_node_ptr<T> ptr = head_node++;
delete ptr.iter;
--this->list_len;
}
}
// 返回长度
node_nums get_len() const {
return list_len;
}
// 修改链表长度 (!!!不安全,考虑简单实现没有继续改进)
node_nums& change_len(const node_nums& len){
list_len = len;
}
// 交换
void swap(forward_linklist<T>& lst1) {
auto lst = lst1;
lst1.head_node = this->head_node;
lst1.change_len(this->get_len());
this->head_node = lst.head_node;
this->list_len = lst.get_len();
}
};
测试代码:
// 测试 int类型;
//本来想顺便测试char*(实际C++中建议不要使用裸指针)
// 没时间算了(晕)
// 遍历一次
void print(forward_linklist<int>* head) {
cout << endl;
int len = head->get_len();
cout << "The current length is " << len << endl;
int pos = 0;
auto ptr = head;
while (pos < len) {
cout << *(ptr->find_node(pos++)) << " ";
}
cout << endl;
}
int main()
{
auto list_head1 = forward_linklist<int>::get_linklist();
auto list_head2 = forward_linklist<int>::get_linklist({ 1,2,3,4,5 });
print(list_head1);
print(list_head2);
cout << "find pos 1 elem : " << *list_head2->find_node(1) << endl << endl;
cout << "______________" << endl;
cout << "delete pos 1 elem : " << *list_head2->delete_node(1) << endl;
cout << "______________" << endl;
print(list_head2);
cout << "insert pos 3 elem (100):" << *list_head2->add_node(100, 3) << endl << endl;
print(list_head2);
cout << "______________" << endl;
cout << "update pos 4 elem" << *list_head2->update_node(1000,4)<< endl;
cout << "______________" << endl;
return 0;
}
双向链表
由于双向链表仅仅是单向链表添加了一个prior指针,许多帖子已经总结得很好了。这里不仔细总结了。可以推荐参考:link1, link2.
拓展与补充:
- 循环链表(将单链表头尾节点连接)
- 双向循环链表(基于双向链表+头尾节点连接)
- 异或链表(一种空间优化后,能够实现双向链表一样目的)
- 广义表(线性表的一种推广)
…
其他许多结构,可以理解为基于以上基础的线性链表的优化。这种优化需要根据场景自行构造。
C++11常见API
这里假设容器存放元素类型为int(由于与vector类似很多,这里不仔细举例)
list
-
迭代器
//注意和vector,array差别,他们都是容器迭代器双向随机迭代器,而这个并不是。 //以下操作list不能使用 iter[i]; //iter -=i ,iter += i , iter + i, iter - i; //iter1 < iter2; iter1 <= iter2; iter1 > iter2; iter1 >= iter2; //可以使用的有 # include <list> vector<list> ls{1,2,3,4,5}; auto iter1 = ls.begin() auto iter2 = ls.end(); iter1++; ++iter1; --iter1; iter1--; *iter; iter1 == iter2; iter1 != iter2; //提供begin(),end(); //rbegin(),rend(); //cbegin(),cend(); //crbrgin(),crend();这些迭代器具体含义和数组篇同理,省略
-
创建和初始化
//参考vector list<int> ls1({1,2,3,4,5}); list<int> ls2{4,10,17,30}; list<int> ls3(-10); list<int> ls4(10 , 5); list<int> ls5(ls4);
-
CURD操作
假设2中容器都存在//容量大小(和vector一样) // empty, size, max_size,resize // 查找 front(); back();//查找第一个或最后一个元素引用 //修改 assign();//基本同vector; assign(n,10);ls2.assign(ls1.begin(),ls1.end()); ls1.assign({-1,-3,-5}); // 插入元素 emplace(),insert(); push_front(),emplace_front(); //与push_back()等想反,在头部插入元素 push_back();emplace_back();//以上函数基本和vector用法一致 splice() //将一个链表该部分插入到另一个链表的指定位置 //删除操作 pop_front(),pop_back(); erase();clear(); //略 remove(val);//删除容器中所有等于 val 的元素 remove_if();//删除容器中满足条件的元素。 unique();//删除容器中相邻的重复元素,只保留一个。 //其他 merge();//合并两个有序链表,保证合并后链表依旧有序 sort();//排序 reverse();//反转容器元素顺序 swap();//交换两个容器中的元素,要求容器以及存储元素类型一致
forward_list
基本和上面内容一样,具体参考reference部分forward_list。
主要注意:
forward_list迭代器基于list的相比是单向的。不支持‘–’(减减)运算符,同样也不是随机的。
还有部分API不同,如erase_after,insert_after,splice_after
典型试题
单一链表场景
链表翻转
- 反转链表
本题建议需要背下所有方案。(摆手:没法,太经典了)
思路
a.就地翻转:就地把next指向修改,注意迭代修改内容。
b.头插法:添加一个辅助的头结点,将元素一个一个插入到头结点部分。
c.递归:需要假设后面已经逆转(递归函数压栈后,返回前处理步骤),需要将尾巴部分改变指针,返回头结点。
当全部返回后,即可。
核心代码
a. 就地翻转
b.头插法/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* prior_node = nullptr, *cur_node = head; // 前一个节点,翻转的当前节点 while (cur_node) { ListNode* next_node = cur_node->next; //翻转后的节点需要临时记录,以便迭代 cur_node->next = prior_node; // 直接翻转 // 移动到下一个 prior_node = cur_node; cur_node = next_node; } return prior_node; // 返回头结点(最后状态是 prior_node - nullptr(cur_node) - nullptr(next_node)) } };
c.递归/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode* reverseList(ListNode* head) { if (head == nullptr) { return head; } ListNode* dummy = new ListNode(0);// 使用一个辅助头结点进行头插法 while (head) { ListNode* next_head = head->next; //保存一下下一个待处理结点 head->next = dummy->next; //将当前节点挂上去 dummy->next = head; head = next_head; } head = dummy->next; //指向新的首个节点,释放辅助头结点内存 delete dummy; return head; } };
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode* reverseList(ListNode* head) { //出发点假设其他的部分已经反转,如何反转其他部分 // 停止递归调用(链表长度为小于等于1时) if (head == nullptr || head->next == nullptr) { return head; } //递归,找到倒数第1个节点(或者是新的头结点) new_head,当前head节点为倒数第二个 ListNode* new_head = reverseList(head->next); head->next->next = head; //改变指向 (由于new_head是新的头,这里写new_head->next = head不对, 注意递归函数返回时候处理部分中间情况应该为 .... ->head-> new_tail<-.....new_head) head->next = nullptr;// head成为新的部分的尾巴 return new_head; //返回改变后的头结点 } };
- 回文链表
思路
a. 存在一个数组中,双指针两头一一匹配即可。(略)
b.利用递归函数:
递归函数一来一回能够使得经过链表每一个节点两次。只要记住对称位置,就能够进行比较
c. 双指针:该方案需要中途修改链表(注意!)
首先用快慢指针找到前半部分尾巴。
翻转后半部分链表
一个一个判断是否回文;
还原链表即可
核心代码
b.递归
c. 双指针(快慢指针)/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { ListNode* front_ptr; public: bool isPalindrome(ListNode* head) { front_ptr = head; return recursively_check(head); } bool recursively_check(ListNode* node) { if (node != nullptr) { if (!recursively_check(node->next)) { // 开始递归,找到最后一个元素比较后返回值 return false; } // 执行过程是先到尾结点后一个,进行判断,后面接着返回 if (node->val != front_ptr->val) { return false; } else { front_ptr = front_ptr->next; // 修改全局对称指针,继续判断 } } return true; // 最后空节点是,先预设为true;递归返回时候继续处理 } };
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode* find_middle_node(ListNode* head) { ListNode* slow = head, * fast = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } return slow; } ListNode* reverse_list(ListNode* head) { ListNode* prev = nullptr, * cur = head; while(cur) { ListNode* cur_next = cur->next; cur->next = prev; prev = cur; cur = cur_next; } return prev; } bool isPalindrome(ListNode* head) { if (head == nullptr) { //排除空链表 return true; } // 找中点或其后面一位 ListNode* middle_node = find_middle_node(head); // 翻转后面部分 ListNode* new_head = reverse_list(middle_node); // 进行判断 ListNode* cur1 = new_head; ListNode* cur2 = head; bool flag = true; ListNode* prev_tail = nullptr; while(cur1 && cur2) { if (prev_tail == nullptr && (cur1->next == nullptr || cur1->next->next== nullptr)) { // 小优化,后面无需找前半部分最后一个节点 prev_tail = cur1; } if (cur1->val != cur2->val) { // 进行比较 flag = false; } //由于之前优化部分,所以必须要找到中点前一个节点,这里不能比较错误直接跳出循环 cur1 = cur1->next; cur2 = cur2->next; } //还原链表 new_head = reverse_list(new_head); prev_tail->next = new_head; return flag; } };
其他补充:
重排链表
k个一组翻转链表
从头到尾打印链表
环形链表
环路检查
思路
a. 哈希:直接记录节点,遇到同样节点直接返回(略)
b.快慢指针:可以看看题解(主要注意计算一下)
核心代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head, *fast = head;
// 找到第一次相遇点
while(fast && fast->next) {
slow = slow ->next;
fast = fast ->next->next;
if (slow == fast) { // 找到后,慢指针路程为1份,快指针路程为1份+k个环的长度+一段
fast = head;
while (fast != slow) { // 将快指针重新置于起点,进行一步一步遍历,抵消1份中不是环的长度
fast = fast->next;
slow = slow->next;
}
return slow;
}
}
return nullptr;
}
};
查找和删除链表节点
- 返回倒数第 k 个节点
思路
快慢指针,比较容易。
这里注意k有效。如果k可能无效。这里另外讨论函数如何定义。
核心代码/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: int kthToLast(ListNode* head, int k) { ListNode* pa = head, *pb = head; for (int i = 0; i < k && pa; ++i) { // 防止k无效,导致pa越界 pa = pa -> next; } while (pa) { pa = pa->next; pb = pb->next; } return pb->val; } };
- 删除链表中的节点
思路
无法删除自己,只好变为自己下一个元素,删除下一个元素(前提自身不是尾结点)。
核心代码/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: void deleteNode(ListNode* node) { node -> val = node->next->val; node->next = node->next->next; } };
- 删除排序链表中的重复元素
思路
类似数组删除重复元素,只是这个可以直接指向下一个节点(LeetCode这里可以暂时考虑不用delete和free。这个是制作这个节点的对象考虑的问题,并且不知道指针这块内存如何获得。如果明确是在堆中获得,需要考虑释放。)
核心代码/** * 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* deleteDuplicates(ListNode* head) { if (head == nullptr || head->next == nullptr) { //空节点或一个节点 return head; } ListNode* slow = head, *fast = head->next; //初始条件 while (fast) { if (slow->val == fast->val) { slow->next = fast->next; } else { slow = slow->next; } fast = fast->next; // 两句判断语句都要做fast更新,只是对slow的更新不一样 } return head; } };
其他补充:
删除排序链表中的重复元素 II
删除链表的节点
链表排序
排序链表
思路
a. 插入排序:使用插入排序,注意代码细节
b. 归并排序:这里介绍自底向上排序方法,另一种自顶向下可以参考这里:
由于其他logn的排序不是变化相邻元素(往往需要随机范文),归并排序能够做到只访问相邻元素。
主要思想是模仿归并排序进行code。
难度在于:代码细节较多,控制要求较高。
c.堆排序:考虑堆结构,将所有节点放入堆中,不停重建链表。
核心代码
a.插入排序
/**
* 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* insertionSortList(ListNode* head) {
if (head == nullptr) {
return head;
}
ListNode* dummy = new ListNode(0 , head); // 头插法进行插入排序
ListNode *prev = nullptr, *cur = head->next; // 排序需要移动元素必须指针
ListNode* last_sort_node = head; // 已经排好序元素分界线
while (cur) {
if (last_sort_node -> val <= cur -> val) {
last_sort_node = last_sort_node -> next; // 扩展有序段,能够保证如果没有调整,last_sort_node->next == cur
} else { //需要进行调整
ListNode* prev = dummy;
while (prev -> next -> val <= cur -> val) { // 找到插入节点前一个位置,方便插入
prev = prev -> next;
}
// 进行调整
last_sort_node->next = cur -> next; // 移除调整节点
cur ->next = prev->next; //单链表插入首先将插入节点next修改(被插入位置修改后,找不到后继节点)
prev->next = cur;
}
cur = last_sort_node -> next; //这里考虑修改后节点cur指向,所以变成了 last_sort_node -> next
}
cur = dummy -> next;
delete dummy;
return cur;
}
};
b. 归并排序
/**
* 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* sortList(ListNode* head) {
if (head == nullptr) {
return head;
}
int len = 0;
ListNode* node = head;
// 计算链表长度
while (node) {
++len;
node = node -> next;
}
// 使用一个辅助头节点
ListNode* dummy = new ListNode(0, head);
// 每次左移一位,表示该处链表归并排序完成
for (int sublen = 1; sublen < len; sublen <<= 1) {
ListNode* prev = dummy, * cur = dummy->next; //有可能第一个节点换掉了,所以这里不能写* cur = head
while (cur) {
ListNode* head1 = cur; //第一段节点的第一个节点, 下面找第二段节点的第一个节点
for (int i = 1; i < sublen && cur->next; ++i) { //防止有一部分节点长度不为sublen
cur = cur->next;
}
ListNode* head2 = cur->next; //当前 cur 为第二段节点的第一个节点前一个节点
cur->next = nullptr; //第一段与第二段断开
cur = head2; //将第二段与后面部分断开,注意考虑第二段cur为空情况
for (int i = 1; i < sublen && cur && cur->next; ++i) {
cur = cur->next;
}
ListNode* next_part = nullptr; //后面部分的头结点,为了防止cur为空,所以相比前一段分隔需要加上一个判断
if (cur) {
next_part = cur->next;
cur->next = nullptr;
}
ListNode* merged = merge(head1, head2); // 合并两端,返回一个头结点
prev->next = merged; //与上一个部分合并
while (prev->next) {
prev = prev->next;
} // prev指向合并后链表段最后一个元素
cur = next_part; //更新最后一个节点
}
}
node = dummy->next;
delete dummy;
return node;
}
ListNode* merge(ListNode* head1, ListNode* head2) { //参考题目:合并两个有序列表题
ListNode* dummy = new ListNode(0);
ListNode* l1 = head1, *l2 = head2, *cur = dummy;
while (l1 && l2) {
if (l1->val < l2->val) {
cur->next = l1;
l1 = l1->next;
}else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
while (l1) {
cur->next = l1;
l1 = l1->next;
cur = cur->next;
}
while (l2) {
cur->next = l2;
l2 = l2->next;
cur = cur->next;
}
cur = dummy->next;
delete dummy;
return cur;
}
};
c.堆排序
/**
* 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 {
struct node {
int val;
ListNode* ptr;
bool operator> (const node& that) const { //构建小根堆,greater,重载大于号
return val > that.val;
}
};
public:
ListNode* sortList(ListNode* head) {
if (head == nullptr) {
return head;
}
priority_queue <node,vector<node>,greater<node>> pq; //注意修改排序方式,默认为大根堆;
while (head) {
pq.push({head->val, head});
head = head->next;
}
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
while (!pq.empty()) {
auto p = pq.top();
pq.pop();
p.ptr->next = nullptr;
cur->next = p.ptr;
cur = cur->next;
}
cur = dummy->next;
delete dummy;
return cur;
}
};
其他补充:
对链表进行插入排序
多个链表场景
链表相交
两个链表的第一个公共节点
思路
a.直接利用哈希表,如果命中一个。我们需要返回命中的那一个。(略)
b.双指针,同时从headA,headB出发,如果对于其中一个指针第一次为空指向另个一节点头继续遍历;当且仅当出现相同节点时候,如果非空:有相交节点(当前指向);否则没有。
核心代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr) { // 只要一个为空节点,保证不相交
return nullptr;
}
ListNode* pA = headA, *pB = headB;
while (pA != pB) { // 注意,最差情况:pA,pB以不同顺序一定会把两个链表访问一次
//不相交一定会有pa == pb ==nullptr
pA = pA == nullptr? headB : pA ->next;
pB = pB == nullptr? headA : pB->next;
}
return pA;
}
};
合并和分隔链表
- 合并两个有序链表
思路
a. 迭代(两个指针同时移动操作即可):注意原节点可能长度不一,导致后面需要添加尾巴。
b. 递归:
核心代码
a. 迭代
b. 递归/** * 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* mergeTwoLists(ListNode* list1, ListNode* list2) { ListNode *p1 = list1, *p2 = list2; ListNode *dummy = new ListNode(0); // 添加一个辅助的头结点,比较好操作 ListNode *p = dummy; while (p1 && p2) { // 两个指针指向有效元素 if (p1 -> val < p2 -> val) { p -> next = p1; p1 = p1->next; } else { p -> next = p2; p2 = p2 -> next; } p = p->next; //当前合并后链表节点指针后移 } // 将剩下节点全部合并到新节点中 while (p1) { p -> next = p1; p1 = p1 -> next; p = p -> next; } while (p2) { p -> next = p2; p2 = p2 -> next; p = p -> next; } // 注意释放辅助节点 p = dummy->next; delete dummy; return p; } };
/** * 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* mergeTwoLists(ListNode* list1, ListNode* list2) { if(list1 == nullptr) { return list2; //返回非空的头结点 } else if (list2 == nullptr) { return list1; } else if (list1 -> val < list2 -> val) { //将问题变为list1的后半部分和list2合并,返回头结点(这里将函数压入系统栈过程,实际在将问题规模简化) list1->next = mergeTwoLists(list1->next, list2); //返回的头结点就是list1的下一个节点,做一定处理(注意每次处理是递归函数返回阶段) return list1; } else { list2 -> next = mergeTwoLists(list2->next,list1); return list2; } return nullptr; } };
- 合并K个升序链表
思路
最直接的思路是两两合并,但是时间复杂度为O( k 2 n k^2n k2n),这里我们给出一些优化方法。
a.分治法,本题可以考虑排序链表中的归并排序方法。
b.使用优先队列,本题考虑排序链表题目中堆排序方法。(基本同排序链表中代码一致,这里省略)
核心代码
a. 分治法(递归版本)/** * 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* mergeKLists(vector<ListNode*>& lists) { return merge(lists, 0, lists.size() - 1); } ListNode* merge_twolist(ListNode* headA, ListNode* headB) { //旧题目:合并两个有序链表,另一种简单写法 if ((!headA) || (!headB)) { return headA ? headA : headB; //返回非空链表头结点 } ListNode head, *cur = &head, *ptra = headA, *ptrb = headB; while (ptra && ptrb) { if (ptra -> val < ptrb ->val) { cur->next = ptra; ptra = ptra->next; } else { cur->next = ptrb; ptrb = ptrb->next; } cur = cur->next; } cur->next = (ptra? ptra : ptrb); return head.next; } ListNode* merge(vector<ListNode*>& lists, int l, int r) { if (l == r) { return lists[l]; //分开 } else if (l > r) { return nullptr; // 没有排序对象 } int mid = l + ((r - l) >> 1); return merge_twolist(merge(lists, l, mid), merge(lists, mid + 1, r)); } };
- 分隔链表
思路
计算长度后,注意分开时候部分长度计算,还有注意断开为空时候条件判断。
核心代码/** * 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: vector<ListNode*> splitListToParts(ListNode* head, int k) { // 首先统计链表长度,然后根据长度进行分隔即可 int len = 0; ListNode* cur = head; while (cur) { cur = cur->next; ++len; } vector<ListNode*> ans(k, nullptr); //每段至少分为part_len个,余下的部分前面部分一个part一份 int part_len = len / k, reminder = len % k; cur = head; for (int i = 0; i < k && cur; ++i) { //注意 cur为空时,计算完长度后,与下一部分断开code会报错 ans[i] = cur; int part_size = part_len + (i < reminder? 1 : 0); //具体每一个部分的长度 for (int j = 1; j < part_size; ++j) { cur = cur->next; } ListNode* cur_next = cur->next; cur->next = nullptr; //断开 cur = cur_next; } return ans; } };
其他补充:
1.合并两个链表
2.分割链表
链表的运算
两数相加
思路
本题代码基本和合并两个链表类似。
注意进位和两个数字位数不等的时候。
核心代码
/**
* 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* addTwoNumbers(ListNode* l1, ListNode* l2) {
// 注意这里数据存储按照类似小端方式
ListNode dummy, *tail = &dummy;
bool carry = false; //表示是否进位
while (l1 || l2) { // l1与l2全部为空,结束循环
int n1 = l1 ? l1 -> val : 0;
int n2 = l2 ? l2 -> val : 0;
int sum = n1 + n2 + carry;
//更新节点数据和进位数据
ListNode* next_cur = new ListNode(sum % 10);
tail->next = next_cur;
tail = tail->next;
carry = sum / 10 == 1 ? true : false;
// 更新l1,l2
if (l1) {
l1 = l1 -> next;
}
if (l2) {
l2 = l2 -> next;
}
}
// 注意可能进位操作,多一位
if(carry) {
tail->next = new ListNode(1);
}
return dummy.next;
}
};
其他补充:
链表求和
两数相加 II
其他题型
- 旋转链表
思路
a 查找倒数第k个节点前一个节点,然后断开。把原始前面部分接在断开的后面部分(注意k>len。所以要先求链表长度)(略)
b 将链表做成环状,然后根据上面断开。(两个都需要注意代码细节)
核心代码/** * 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* rotateRight(ListNode* head, int k) { if (k == 0 || head == nullptr || head->next == nullptr) { //排除不需要继续算的数据 return head; } // 由于要连接成环,我们需要定位在最后一个节点上。 int len = 1; ListNode* cur = head; while (cur -> next) { ++len; cur = cur->next; } // 如果旋转长度为len的倍数,直接返回 k = len - k % len; if (k == len) { return head; } //封闭成环 cur -> next = head; while (k--) { cur = cur->next; } // 断开并且记录新的头结点 head = cur->next; cur->next = nullptr; return head; } };``` >时间复杂度O(n)。 空间复杂度O(1)。
- 链表随机节点
思路
a. 最简单思路使用数组记录链表每一个节点,然后随机(略)
b. 这里对空间优化(假设数据足够大),主要使用蓄水池算法:
对第i个节点,每次投掷一个随机数[0,i);等于0返回该值,否则保留原始值。
如果要随机k个数,改进为:
1.创建一个长度为 k 的数组(蓄水池)用来存放结果;
2.初始化为 arr 的前 k 个元素。
3.从 k+1 个元素开始遍历直到数组结束,算法对每一个下标生成一个随机数 j∈[0,i), 如果 j < k, 那么蓄水池的第 j 个元素被替换为 arr 的第 i 个元素。
核心代码/** * 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 { ListNode* head; public: Solution(ListNode* head) { this->head = head; } int getRandom() { int ans = 0, pos = 1; for (auto node = head; node; pos++, node = node -> next) { if (rand() % pos == 0) { ans = node -> val; //改变数据 } } return ans; } }; /** * Your Solution object will be instantiated and called as such: * Solution* obj = new Solution(head); * int param_1 = obj->getRandom(); */
- 复制带随机指针的链表
思路
本题可以有比较有技巧性的解法。
a. 最简单的解法是使用哈希表,首先将源节点和新节点一一对应记录(同时生成);
第二次遍历时候,一个一个连接好即可。
b. 上一题使用空间复杂度为O(n),如果要求空间复杂度为O(1),且允许修改元链表,如何优化?
优化方法:
1.原地拷贝节点:如1->3->4变为1->1->3->3->4->4。
2.将新建的拷贝节点的随机指针设置好。
3.分开拷贝节点部分(随机节点可以不动,只需要改变next)
c. 迭代回溯做法参考题解。
核心代码
a.哈希表直接求解
b 直接迭代:/* // Definition for a Node. class Node { public: int val; Node* next; Node* random; Node(int _val) { val = _val; next = NULL; random = NULL; } }; */ class Solution { public: Node* copyRandomList(Node* head) { if (head == nullptr) { return head; } unordered_map<Node*, Node*> mp; // 创建映射关系[旧节点,新节点] for(auto node = head; node; node = node -> next) { Node* new_ptr = new Node(node->val); mp[node] = new_ptr; } //依次串好 for (auto[ori_node , new_node] : mp) { new_node->next = mp.count(ori_node->next) ? mp[ori_node->next] : nullptr; //注意指向空节点,mp内没有 new_node->random = mp.count(ori_node->random) ? mp[ori_node->random] : nullptr; } return mp[head]; } };
/* // Definition for a Node. class Node { public: int val; Node* next; Node* random; Node(int _val) { val = _val; next = NULL; random = NULL; } }; */ class Solution { public: Node* copyRandomList(Node* head) { if (head == nullptr) { return nullptr; } //首先原地拷贝 for (auto node = head; node; node = node->next->next) { Node* new_node = new Node(node->val); new_node -> next = node -> next; node->next = new_node; } //然后将new_node的随机指针设置好 for (auto node = head; node; node = node->next->next) { Node* new_node = node->next; new_node->random = node->random ? node->random->next : nullptr; } Node* new_head = head->next; // 记录新的头结点 // 最后将两个部分分离 for (auto node = head; node; node = node->next) { //分离时候由于指针改变,只要移动一次 Node* new_node = node->next; node->next = new_node->next; new_node->next = new_node->next ? new_node->next->next : nullptr; } return new_head; } };
References:
- 基础知识部分参考:https://www.bilibili.com/video/BV13f4y1k7kw
- list部分: https://en.cppreference.com/w/cpp/container/list
- forward_list部分:https://en.cppreference.com/w/cpp/container/forward_list
- 题目来源:https://leetcode-cn.com/
- 其他推荐阅读:https://zhuanlan.zhihu.com/p/150871816