Day3
链表理论基础
- 链表是通过指针连接的线性结构,链表的节点在内存中不连续存在(不像数组)
- 链表的每一个节点由两部分组成:数据域+指针域
- 指针域指向下一个节点,最后一个节点的指针指向nullptr(循环链表除外)
链表的类型
单链表
- 单链表的节点由两部分组成:数据+后向指针
- 后向指针指向链表的下一个节点
- 由于单链表只有后向指针,只能按一个方向访问元素
双链表
- 单链表的节点由三部分组成:数据+前向指针+后向指针
- 前向和后向指针分别指向当前节点的上一个和下一个节点
- 由于双链表有两个方向的指针,可以随意前后访问元素
循环链表
- 循环链表只是连接方式上有所不同,既可以是单链表也可以是双链表
- 循环链表中最后一个节点的后向指针指向的是头节点(形成一个环)
- 如果该链表是双链表,那么头指针的前向指针指向最后一个元素
链表的存储方式与定义
-
链表的节点在内存中不连续存在。(不像数组在内存中是连续分布)
-
常见链表的定义如下
template<typename T> struct ListNode { ListNode* prev; // 前向指针 ListNode* next; // 后向指针 T data; // 数据域 ListNode(T x) : prev(nullptr), next(nullptr), data(x) { }
链表的操作
插入节点
-
以单链表为例,要在如下位置插入一个节点F
- 首先遍历到C的位置,随后修改F的
next
指针指向D,然后修改C的next
指针指向F,完成链表的插入。- 不使用额外空间,先修改C行不行: 不行,由于链表靠指针连接在一起,先修改C的
next
指向F后,我们失去了唯一访问节点D的入口,无法在后续让F的next
指针指向D。 - 就想先改C,可以用额外空间: 在遍历到C的位置时,使用一个 额外指针 指向D。随后修改C的
next
指向F。由于先前用一个指针指向D,此时可以修改F的next
指向D完成插入。
- 不使用额外空间,先修改C行不行: 不行,由于链表靠指针连接在一起,先修改C的
- 首先遍历到C的位置,随后修改F的
删除节点
-
以单链表为例,要删除节点D
- 要删除,只要将C节点的
next
指针指向E就可以了。 - 注意此时D仍然存在于内存,只是修改
next
的这个行为把D从链表中“排除”了,但没有删除- 如果是C/C++,这里就需要手动
delete
掉这个节点,避免内存泄漏 - 其他语言有垃圾回收,不用手动释放了
- 如果是C/C++,这里就需要手动
- 要删除,只要将C节点的
链表时间开销
插入开销
- 插入分为两部分:查找+插入
- 查找的时间是
O(n)
- 因为链表的分布特性,不支持随机访问,只能从前向后遍历到我们要插入的位置。最坏情况下,要插入到最后一个节点,我们要遍历前面的所有n-1个元素。
- 插入的时间是
O(1)
- 只需要修改节点的
next
指针就可以完成插入
- 只需要修改节点的
删除开销
- 删除和查找一样,也分为两部分:查找+插入
- 查找的时间是
O(n)
- 从前向后遍历到我们需要删除的节点。最坏情况下删除最后一个节点,要遍历前面的所有n-1个元素。
- 删除的时间是
O(1)
- 只需要修改元素的
next
指针就可以完成删除
- 只需要修改元素的
和数组相比
203. Remove Linked List Elements
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head == nullptr)
return head;
ListNode* fakeHead = new ListNode(-1);
fakeHead->next = head;
ListNode* curr = fakeHead;
while(curr->next != nullptr)
{
if(curr->next->val == val)
{
ListNode* toDelete = curr->next;
curr->next = toDelete->next;
delete toDelete;
}
else
curr = curr->next;
}
head = fakeHead->next;
delete fakeHead;
return head;
}
};
707. Design Linked List
class MyLinkedList {
public:
struct ListNode {
int val;
ListNode* next;
ListNode();
ListNode(int _val, ListNode* _next) {
val = _val;
next = _next;
}
ListNode(int _val) {
val = _val;
next = nullptr;
}
};
ListNode* fakeHead;
int length;
MyLinkedList() {
fakeHead = new ListNode(-1);
length = 0;
}
int get(int index) {
if (index < 0 || index >= length)
return -1;
ListNode* curr = fakeHead;
while (index > 0) {
curr = curr->next;
--index;
}
return curr->next->val;;
}
void addAtHead(int val) {
ListNode* newNode = new ListNode(val, fakeHead->next);
fakeHead->next = newNode;
++length;
}
void addAtTail(int val) {
ListNode* curr = fakeHead;
while(curr->next != nullptr) {
curr = curr->next;
}
ListNode* newNode = new ListNode(val);
curr->next = newNode;
++length;
}
void addAtIndex(int index, int val) {
if (index == 0) {
addAtHead(val);
return;
} else if (index == length) {
addAtTail(val);
return;
} else if (index > length)
return;
ListNode* curr = fakeHead;
while (index > 0) {
curr = curr->next;
--index;
}
ListNode* newNode = new ListNode(val, curr->next);
curr->next = newNode;
++length;
}
void deleteAtIndex(int index) {
if(index >= length || index < 0)
return;
ListNode* curr = fakeHead;
while (index > 0) {
curr = curr->next;
--index;
}
ListNode* tmp = curr->next;
curr->next = curr->next->next;
delete tmp;
tmp = nullptr;
--length;
}
};
206. Reverse Linked List
- 反转链表,一道比较经典的题目,主要是要处理好指针间的关系
- 我们利用三个指针反转链表,通过名字能直观判断出指向的都是谁,分别是
prev
,curr
,next
- 虽然用了三个指针,但注意在循环的时候,我们每次只反转prev和curr,next主要起到一个跟踪的作用。要是把next也反转了,就丢了对后续节点的访问入口了
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr)
return head;
ListNode* prev = nullptr;
ListNode* curr = head;
ListNode* next = head->next;
while(curr->next)
{
curr->next = prev;
prev = curr;
curr = next;
next = next->next;
}
curr->next = prev;
head = curr;
return head;
}
};