移除链表元素
思路
这个题属于基本的操作链表的题,比较简单,要知道移除某个元素需要定位这个元素的前一个,这个和数组要区分开。
ListNode* removeElements(ListNode* head, int val) {
if(head == nullptr) return head;
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;//虚拟头指针
ListNode* p = dummyHead;//工作指针
while(p->next != nullptr){
if(p->next->val == val){
ListNode* temp = p->next;
p->next = p->next->next;
delete temp;
}
else{
p = p->next;
}
}
head = dummyHead->next;
delete dummyHead;
return head;
}
首先,我们需要一个虚拟的头节点,将它放在原来头节点的前面( 也就是将虚拟指针的指针域指向原来头节点 ),这样的话移除头节点和其他节点就不用两种处理的逻辑了,为什么循环的判断条件是 p->next != nullptr 而不是 p !=nullptr 呢,因为我们只需定位到需要删除节点的前一个节点就可以了。
设计链表
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。
示例:
输入 ["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"] [[], [1], [3], [1, 2], [1], [1], [1]] 输出 [null, null, null, null, 2, null, 3] 解释 MyLinkedList myLinkedList = new MyLinkedList(); myLinkedList.addAtHead(1); myLinkedList.addAtTail(3); myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3 myLinkedList.get(1); // 返回 2 myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3 myLinkedList.get(1); // 返回 3
思路
class MyLinkedList {
public:
struct LinkNode {
int val;
LinkNode* next;
LinkNode(int val) : val(val), next(nullptr){}
};
MyLinkedList() {
_dummyHead = new LinkNode(0);
_size = 0;
}
int get(int index) {
if(index < 0 || index > (_size - 1)) return -1;
LinkNode* p = _dummyHead->next;
while(index--){
p = p->next;
}
return p->val;
}
void addAtHead(int val) {
LinkNode* newNode = new LinkNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
void addAtTail(int val) {
LinkNode* newNode = new LinkNode(val);
LinkNode* p = _dummyHead;
while(p->next != nullptr){
p = p->next;
}
p->next = newNode;
_size++;
}
void addAtIndex(int index, int val) {
if(index > _size) return;
if(index < 0) index = 0;
LinkNode* newNode = new LinkNode(val);
LinkNode* p = _dummyHead;
while(index--){
p = p->next;
}
newNode->next = p->next;
p->next = newNode;
_size++;
}
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkNode* p = _dummyHead;
while (index--){
p = p->next;
}
LinkNode* tmp = p->next;
p->next = p->next->next;
delete tmp;
tmp = nullptr;
_size--;
}
private:
int _size;
LinkNode* _dummyHead;
};
我们需要定义一个链表的节点结构体,每个元素都是一个链表的节点,包含一个指针域和一个数据域以及构造函数。然后初始化我们的链表,这里定义的头结点不是真正的头节点,而是一个虚拟的头节点,为了让操作头节点的逻辑和其他节点的逻辑是一样的。get函数是获取第index个节点的数据,注意index是从0开始,index为0也就是获取头节点的数据。比较简单,先进行边界条件的判定,直接遍历到第index节点就可以了。addAtHead函数和addAtIndex函数是一个道理,放在一起来说,在链表中插入节点,重点是找到要插入节点的前一个节点,也就是p->next是要删除的节点,然后将新节点的指针域存入p->next,再将前一个节点的指针域指向新的节点,顺序不能错,很多人就在这里出现错误,顺序一定一定是不能颠倒的。addAtTail函数是在链表的末尾插入节点,这个比较好理解,就直接找到最后一个元素然后将最后一个元素的指针域指向新节点就好。deleteAtIndex函数是删除第index个结点,如果index 大于等于链表的长度,直接return,注意index是从0开始的。找到要删除节点的前一个,然后将要删除的节点用个变量临时存储起来,为了等会释放内存,将前一个节点的指针域指向删除节点的下一个节点,也就是p->next = p->next->next;就可以了。
反转链表
思路
双指针法
ListNode* reverseList(ListNode* head) {
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
ListNode* temp = cur->next; // 保存一下 cur的下一个节点,为了移动cur
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
首先,双指针法我们必须要明白两个指针的含义是什么,在这道题中,两个指针一前一后,将后面那个指针的指针域指向前面的指针,这样,就完成了反转(就相当于把原来的指针换了个方向)。在while循环中,结束条件如何写?要看我们结束的时候是什么样子,如下图,当pre指向最后一个节点,cur指向nullptr时结束,所以只要cur不为空指针就继续循环。在循环中,我们需要一个临时的变量来存储cur的下一个节点,因为cur要改变,不然就找不到后续的节点了。要注意的是,在更新cur和pre的时候一定是先更新前面的pre,再更新cur,顺序不能颠倒,不然cur移动到下一个节点了,pre找不到了,前面的先走,后面的跟上。
递归法
/**
* 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* reverse(ListNode* cur,ListNode* pre){
if(cur == nullptr) return pre;
ListNode* tmp = cur->next;
cur->next = pre;
return reverse(tmp, cur);
}
ListNode* reverseList(ListNode* head) {
return reverse(head, nullptr);
}
};
递归法的逻辑和双指针法其实是一样的,只是刚看会比较懵,我们来分析一下。首先进入reverseList函数,调用我们的递归函数,这里要明白我们的参数应该填什么?在这道题,最核心的变量就是cur和pre,所以递归函数的参数是cur和pre,在reverseList函数调用的时候只有一个头结点,跟双指针法对应,所以应该填head和nullptr。在递归函数的内部,我们得设置结束的条件,当cur指向空的时候,pre指向最后一个节点的时候(也就是新链表的头指针)结束,所以返回的是pre,然后就是用临时变量存储cur的下一个节点的值,反转指针的指向,再进行递归调用,参数根据双指针法cur = tmp, pre = cur,这里就没有顺序的问题了。