目录
链表理论基础
不连续存储,散乱分布,节点与节点之间由指针线性联系在一起,每个节点包含数据域和指针域
单链表,头结点head,尾结点指向null(即空指针)
双链表,prev和next,头节点的prev和尾节点的next指向null
循环链表(约瑟夫环问题),首尾相连
链表定义
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
构造函数也可默认,此时不会初始化任何成员变量
// 自己定义的构造函数初始化结点
ListNode* head = new ListNode(5);
// 默认构造函数初始化结点
ListNode* head = new ListNode();
head->val = 5;
链表增删结点O(1),查询结点O(n),数据量不固定
数组增删O(n),查询O(1),数据量固定
203.移除链表元素
第一思路:没理解头节点为什么能是一个数组,其他节点从哪里来的,以及返回新的头节点是什么意思,然后便卡壳了。
难点:假设就是一个一个节点,怎么找到node.val==val的这个节点,咋遍历。
状态:未实现
解决办法:
就看成给出了头节点是head的链表,链表元素依次是数组值就行了,大体上,别纠结。要删除节点,就得找到上一个节点,即cur设为本来要删除节点的上一个,然后用next就好操作了。否则因为单链表,直接让当前节点为要删除的节点的话,找不到上一个。注意判断当前结点是否为空,否则操作空指针会报错。设置临时指针cur,保证原比如head不变。
1.不设置虚拟头结点(头结点与非头节点处理方式不一样,头节点直接next为下一个)
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头节点
while (head != NULL && head->val == val ) { // while而不是if,可能需要持续移除
ListNode* tmp = head;
head = head->next; //头结点删除直接移向下一个结点即可
delete tmp; //C++需要手动删除多余结点,注意要额外设置一个节点来存要删除的
}
// 删除非头结点
ListNode* cur = head; // 当前节点为要删除节点的上一个,同时另设cur,head依旧是头节点
while (cur != NULL && cur->next != NULL) { // cur->next也不为NULL,毕竟还要判断它的val值
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else { // 不是,则往后移动
cur = cur->next;
}
}
return head;
}
};
2.设置虚拟头结点(统一操作,返回值为dummyHead->next,原head有可能已经被删除了)
时间复杂度O(n)
空间复杂度O(1)
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 设置虚拟头结点
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head;
ListNode* cur = dummyHead; // 要删除节点的前一个节点
while (cur->next != NULL) { // 就不用判断dummyHead是不是为空了,前面有进行初始化
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
}else {
cur = cur->next;
}
}
head = dummyHead->next; // 返回值不是纯粹的head,head有可能已经在循环中被删除了
delete dummyHead; // 删掉设置的虚拟节点,释放内存
return head;
}
};
707.设计链表
第一想法:链表哪来的下标,好的,你题目假设,那这个下标是默认已经有了?还是要自己设变量。如何初始化MyLinkedList对象,在函数里和在struct里一样么。
难点:具体写函数时,头结点怎么设置定义,如何定义函数需要的那些结点,索引的确定是怎样的,如何初始化链表函数
状态:瞎写一通,未实现
解决方法:
自定义_size来记录链表大小,插入++,删除--。遍历链表while(index--),注意这里的index从0开始(判断while循环有没有写对,可举极端例子,比如在0个结点这种)。初始化对象(虚拟头结点给初始化了),另外依然要struct结构体,在这里用到了私有成员变量。涉及到访问第index结点时,检查index是否合法。
插入节点,先右边再左边。让cur->next为第index个结点。
时间复杂度:除了涉及index的操作为O(n),其他均为O(1)
空间复杂度:O(1)
class MyLinkedList {
public:
// 自定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点是虚拟头结点
_size = 0; // 记录链表大小的变量,"_"可表示类的私有成员变量,区分局部变量
}
// 获取第index个节点值
int get(int index) {
LinkedNode* cur = _dummyHead; // 额外定义一个cur来遍历列表,否则直接操作头结点的话,头结点的值改变了,还如何返回头结点
if (index < 0 || index > (_size - 1)) { // 无效情况的考虑
return -1;
}
while (index--) { //n从0开始,判断循环是否写对,可举一个极端例子,比如这里要返回第0个结点,即头结点,符合
cur = cur->next;
}
return cur->next->val;
}
// 在头部插入节点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next; // 先右边再左边,否则记录不了插入节点位置的后一个节点
_dummyHead->next = newNode;
_size++; // 链表大小相应更新
}
// 在尾部插入节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) { // cur->next==NULL时,cur便来到了最后一个结点
cur = cur->next;
}
cur->next = newNode; // 新节点默认指向null
_size++;
}
// 在第index个节点前插入
void addAtIndex(int index, int val) {
if (index > _size) return;
if (index < 0) index = 0; // 如果index等于链表长度,则新插入节点为尾结点;如果index小于0,则在头部插入节点
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while (index) { //while(index)通过--来实现遍历,依然可举极端例子来判断是否写对
cur = cur->next;
index--;
} // 让cur->next是第n个结点
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个结点
void deleteAtIndex(int index) {
if (index < 0 || index > _size -1) {
return; // 直接return
}
LinkedNode* cur = _dummyHead;
while (index) {
cur = cur->next;
index--;
}
LinkedNode * tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
tmp = nullptr;
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
206.反转链表
第一思路:从头遍历链表,然后新开一个节点指向head,在获取第一个元素后不断在他前面插入节点
难点:涉及到节点记录缺失的问题,节点一边需要再原本节点基础上往前走,一边又要用来插入,很混乱
状态:未实现
解决办法:
1.双指针(指向箭头换了个方向,一个指针往前走,另一个指针提供箭头反向,一个结点一个结点的反向,到达末尾的时候返回即返回了头结点)(无需新定义一个链表,只需改变next指针的指向)(这里不需要虚拟头结点)(哪步先哪步后,一错,可能记录就缺失了,这里就是要先保存cur->next,因为下一步就会更改cur->next,否则原cur就无法继续走了)
时间复杂度O(n)
空间复杂度O(1)
public:
ListNode* reverseList(ListNode* head) {
// 双指针法
ListNode* cur = head;
ListNode* prev = nullptr;
ListNode* tmp; // 保存cur的下一个节点
while (cur) { // cur==nullptr时,结束循环
tmp = cur->next;
cur->next = prev; // 更改next指向
prev = cur; // 顺序!
cur = tmp;
}
return prev;
}
};
2.递归(照着双指针的思路来写即可,主要是初始化的写法变了,逻辑还是一样的)
时间复杂度O(n),递归了n次,递归处理链表中的n个结点
空间复杂度O(n),递归深度为n,递归调用了n层栈空间
class Solution {
public:
ListNode* reverse(ListNode* cur, ListNode* prev) {
if (cur == nullptr) return prev;
ListNode* tmp = cur->next;
cur->next = prev;
return reverse(tmp, cur);
}
ListNode* reverseList(ListNode* head) {
return reverse(head, nullptr);
}
};
一句话,路漫漫其修远兮.现在每道题从自己想思路到最终看视频看文章写代码解决花的时间还是有点长,一个多小时,理解得比较慢。不过才开始,没有关系,坚持下去,后面速度应该会慢慢提升的。