📁206. 反转链表
对于每一个节点cur,都是将next节点指向cur,cur节点指向上一个节点head。因此可以采用递归的策略,从后往前进行上述操作,期间记录最后一个节点并返回。
我是将递归分为3类:
-
前序递归:在递归之前进行处理。
-
中序递归:在递归中进行处理。
-
后续递归:在递归后处理。
本地就是一个后续递归的操作,地递归到最后一层,从后往前进行处理。
cpp
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr)
return head;
ListNode* newHead = reverseList(head->next);
ListNode* next = head->next;
head->next = next->next;
next->next = head;
return newHead;
}
📁160. 相交链表
我们假设A链表总结点个数是a,B链表总结点个数是b,如果存在公共节点node,那么我们假设公共节点后面所有节点个数是c。
指针 cur1 先遍历完链表 headA
,再开始遍历链表 headB
,当走到 node
时,共走步数为:
a+(b−c)
指针 cur2 先遍历完链表 headB
,再开始遍历链表 headA
,当走到 node
时,共走步数为:
b+(a−c)
那么可以列式,并有两种情况:
a+(b−c)=b+(a−c)
1. 如果两链表有公共节点(c > 0):节点 cur1 和 cur2 指向公共节点node
2. 如果不存在公共节点(c = 0):节点 cur1 和 cur2 都指向nullptr
cpp
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* cur1 = headA , *cur2 = headB;
while(cur1 != cur2)
{
cur1 = cur1 != nullptr ? cur1->next : headB;
cur2 = cur2 != nullptr ? cur2->next : headA;
}
return cur1;
}
📁234. 回文链表
一个简单的思路就是取出链表中的元素,放到数组中,使用双指针判断数组是否是回文。
cpp
bool isPalindrome(ListNode* head) {
vector<int> ret;
while(head)
{
ret.push_back(head->val);
head = head->next;
}
int right = ret.size() - 1 , left = 0;
while(left < right)
{
if(ret[left++] != ret[right--])
return false;
}
return true;
}
📁141. 环形链表
我们来假设一种情况,在一条无限长的跑道上,小明每秒走x步,小妹每秒走x+1步,并且小美在小明后面追赶,如果情况成立小美一定能追上小明,因为他们之间的距离每次都1。
对于该题目,采用双指针思路,如果链表有环,那么slow指针进入换后就一定能被fast指针追上。
cpp
bool hasCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
return true;
}
return false;
}
📁142. 环形链表 II
符号定义与初始条件
- 头节点 (
head
):链表的起点。 - 环入口节点 (
A
):链表开始形成环的节点。 - 相遇点 (
B
):快慢指针第一次相遇的位置。 - a:头节点到环入口节点的距离。
- b:环入口节点到相遇点的距离。
- c:相遇点到环入口节点的剩余距离。
- 环周长 :
n = b + c
(环的总长度)。 - 快指针速度:每次移动 2 步。
- 慢指针速度:每次移动 1 步。
Sfast=2⋅Sslow⟹a+k(b+c)+b=2(a+b)
将上述方程化简:
a+k(b+c)+b=2a+2b⟹k(b+c)=a+b**⟹ a=(k−1)(b+c)+c**
- 等于 c 加上
(k - 1)
圈环的周长。 - 若
k = 1
,则a = c
,即从头节点到环入口的距离等于相遇点到环入口的距离。
cpp
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
while(slow != head)
{
slow = slow->next;
head = head->next;
}
return head;
}
}
return nullptr;
}
📁2. 两数相加
本题就是一道类似高精度的问题,但是本题相较简单,我们只需要从后往前遍历即可,每次取出两个个位数相加,对结果进行模除操作结果放到新节点中即可。
此外,需要注意的是,最后需要判断最后一位数相加是否还需要进位。
cpp
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* newHead = new ListNode();
ListNode* tail = newHead;
int tmp = 0;
while(l1 != nullptr || l2 != nullptr)
{
if(l1)
{
tmp += l1->val;
l1 = l1->next;
}
if(l2)
{
tmp += l2->val;
l2 = l2->next;
}
tail->next = new ListNode(tmp % 10);
tmp /= 10;
tail = tail->next;
}
if(tmp)
tail->next = new ListNode(tmp);
tail = newHead->next;
delete newHead;
return tail;
}
📁 21. 合并两个有序链表
最简单的一个思路就是创建一个新的链表,然后遍历两个链表,将较小值的节点尾插到新节点中。
cpp
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* cur1 = list1 , *cur2 = list2;
ListNode* newHead = new ListNode();
ListNode* tail = newHead;
while(cur1 && cur2)
{
if(cur1->val < cur2->val)
{
tail->next = cur1;
tail = tail->next;
cur1 = cur1->next;
}
else
{
tail->next = cur2;
tail = tail->next;
cur2 = cur2->next;
}
}
while(cur1)
{
tail->next = cur1;
tail = tail->next;
cur1 = cur1->next;
}
while(cur2)
{
tail->next = cur2;
tail = tail->next;
cur2 = cur2->next;
}
tail = newHead->next;
delete newHead;
return tail;
}
也可以采用递归的方法。
-
递归函数的返回值:返回合并后链表的头结点。
-
递归函数结束条件:如果有一个链接节点为空,返回另一个节点。
-
递归过程:比较两个链表的节点,取出较小值作为当前轮次的头结点,它的next指向递归的返回值,即两个链表剩下节点合并后链表的头结点。
cpp
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(!list1) return list2;
if(!list2) return list1;
if(list1->val < list2->val)
list1->next = mergeTwoLists(list1->next , list2);
else
list2->next = mergeTwoLists(list1 , list2->next);
return list1->val < list2->val ? list1 : list2;
}
📁 19. 删除链表的倒数第 N 个结点
从头节点开始对链表进行一次遍历,得到链表的长度 L。随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L− n + 1 个节点时,它就是我们需要删除的节点。
cpp
ListNode* removeNthFromEnd(ListNode* head, int n) {
int sz = 0;
ListNode* cur = head;
while(cur)
{
++sz;
cur = cur->next;
}
//创建新的头结点方便处理只有一个节点的特殊情况
ListNode* newhead = new ListNode( 0 , head);
cur = newhead;
for(int i = 0 ; i < sz - n; ++i)
{
cur = cur->next;
}
cur->next = cur->next->next;
return newhead->next;
}
📁 24. 两两交换链表中的节点
本题我们可以看成反转链表的一种变种,只不过是变成两两交换了,第一个节点head , 第二个节点next,head节点next指向剩余链表中交换后的链表的头结点,next节点的next指向head节点。
cpp
ListNode* swapPairs(ListNode* head) {
if(!head || !head->next)
return head;
ListNode* next = head->next;
head->next = swapPairs(next->next);
next->next = head;
return next;
}
📁 25. K 个一组翻转链表
就是将 n / k 个组内的节点进行头插,然后将剩下的节点在链入链表中即可。
cpp
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* newHead = new ListNode();
ListNode* tail = newHead; //组内最后一个节点
ListNode* cur = head;
int n = 0;
while(cur)
{
++n , cur = cur->next;
}
n /= k;
cur = head;
for(int i = 0 ; i < n ; ++i)
{
//每组第一个节点经过翻转后变成最后一节点
ListNode* tmp = cur;
for(int j = 0 ; j < k ; ++j)
{
ListNode* next = cur->next;
cur->next = tail->next;
tail->next = cur;
cur = next;
}
tail = tmp;
}
if(cur)
tail->next = cur;
return newHead->next;
}
📁 138. 随机链表的复制
对于本题最苦难的地方就在于随机指针的指向问题了,因为random可能指向一个还没有创建的节点node,如果我们直接创建,并使得random指向node,那么node之前的节点的next指针怎么指向node,因此我们需要一个东西来记录下来。
我们可以使用unordered_map来记录原节点和新节点的对应关系,这样random指向一个没有创建的节点node时,node可以直接创建,并且node节点之前的节点prev的next指针可以通过hash找到node节点,并成功指向。
cpp
class Solution {
public:
unordered_map<Node* , Node*> hash;
Node* copyRandomList(Node* head) {
if(head == nullptr)
return head;
if(!hash.count(head))
{
Node* newNode = new Node(head->val);
hash[head] = newNode;
newNode->next = copyRandomList(head->next);
newNode->random = copyRandomList(head->random);
}
return hash[head];
}
};
📁 148. 排序链表
最简单的方法就是节点排序,然后创建新的链表,按值大小从后往前链接节点。我们使用map记录值和节点直接的映射关系,因为底层是红黑树,即平衡二叉搜索树,所以遍历时是从小到大遍历的,multimap可以保证我们存在相同值。
cpp
ListNode* sortList(ListNode* head) {
if(head == nullptr || head->next == nullptr)
return head;
multimap<int , ListNode*> hash;
ListNode* cur = head;
while(cur)
{
hash.insert(make_pair(cur->val , cur));
cur = cur->next;
}
ListNode* newHead = new ListNode();
ListNode* tail = newHead;
for(auto& [key , node] : hash)
{
tail->next = node;
tail = tail->next;
}
tail->next = nullptr;
return newHead->next;
}
📁 23. 合并 K 个升序链表
对于链表,我可以将翻转看成将每个节点从后往前进行头插。那么本题就简单了,我们只需要头插节点即可。
"如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序",可知我们需要翻转组数目 = n / k。
此外,进行头插后,我们还需要记录下来每组第一个节点,每组进行反转后第一个节点变为了最后一个节点,它的next节点应该指向下一组翻转节点。
cpp
struct cmp
{
bool operator()(const ListNode* l1 , const ListNode* l2)
{
return l1->val > l2->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
std::priority_queue<ListNode* , std::vector<ListNode*> , cmp> heap;
for(ListNode* node : lists)
if(node != nullptr)
heap.push(node);
ListNode* newHead = new ListNode();
ListNode* tail = newHead;
while(!heap.empty())
{
ListNode* node = heap.top(); heap.pop();
tail->next = node;
tail = tail->next;
if(node->next != nullptr)
heap.push(node->next);
}
return newHead->next;
}
📁 146. LRU 缓存
本题就是将将新插入或刚被调用的元素插入到链表头部,当容量满时淘汰掉链表尾部元素,即最近最少被使用的元素。
为什么使用unorder_map,因为哈希系列查找速度是O(1),而链表的查找速度是O(N),为了提高效率我们使用哈希来查找存储 <key,val>元素的链表节点的iterator。
cpp
class LRUCache {
public:
typedef list<pair<int,int>>::iterator it;
LRUCache(int capacity) {
_capacity = capacity;
}
//判断是否缓存
//找到了 将其剪切到链表的头部位置
int get(int key) {
auto kv = _hash.find(key);
if(kv != _hash.end())
{
//push过 已被缓存
_list.splice(_list.begin() , _list , kv->second);
return kv->second->second;
}
return -1;
}
//1. 首先查找LRU中是否缓存了
//2. 如果存在, 更新val 并且放到链表的最前方
//3. 如果不存在 在判断链表是否未满
//i. 如果链表满了 尾删
//ii. 其次 头插并记录映射
void put(int key, int value) {
auto kv = _hash.find(key);
if(kv != _hash.end())
{
//更新
kv->second->second = value;
_list.splice(_list.begin() , _list , kv->second);
}
else
{
if(_list.size() == _capacity)
{
//尾删一个元素
_hash.erase(_list.back().first);
_list.pop_back();
}
//头插新元素
_list.emplace_front(key , value);
_hash[key] = _list.begin();
}
}
private:
int _capacity = 0;
unordered_map<int , it> _hash;
list<pair<int , int>> _list;
};