链表
链表的节点的定义:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
203.移除链表元素
题目链接: link
链表中移除头节点的方式和删除非头节点的方式是不一样的,
如果要保持方法一致,就需要使用虚拟头节点来指向。(代码更为简洁)
在每次操作的时候还应该注意考虑是否为空指针的情况,防止操作错误。
不使用虚拟头节点,两种情况分开讨论时:
(随想录的代码)
删除头节点时使用while循环而不是if是因为有可能删除这个头节点后,下一个变成头节点的位置仍然是需要进行删除的,因此便需要使用while来进行判定。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
while (head != NULL && head->val == val) { // 注意这里不是if
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头结点
ListNode* cur = head;
while (cur != NULL && cur->next!= NULL) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
return head;
}
};
时间复杂度: O(n)
空间复杂度: O(1)
虚拟头节点的方法:
遍历的时候头节点是不能改的,创建了一个cur的临时指针来进行遍历;因为如果直接用头节点进行操作,后面就返回不回去原先位置了,用一个cur来表示头节点,然后用它来进行操作和修改。
不用虚拟头节点去进行遍历!
cur是要等于head开始的,因为如果我们要删除一个元素节点,那一定是从这个节点的上一个开始指向过去的。才能删除cur->next
用增加虚拟头节点可以统一对于链表的操作
注意在返回的时候一定是返回的我们设定的虚拟头结点的下一个节点,而不是head,因为最开始的head我们可能也删除掉了,在处理完以后,虚拟头节点的下一个节点才是我们真正的节点。
要使用虚拟头节点,也就需要新分配一个空间来用于给虚拟头节点:
NEW运算符的使用:
new+数据类型(初值),返回值为申请空间的对应数据类型的地址
与malloc相比较:
new/delete 是C++中的运算符。malloc/free 是函数
malloc 申请内存空间时,手动计算所需大小,new 只需要类型名,自动计算大小
malloc 申请的内存空间不会初始化,new 可以初始化(需要调用构造函数)
malloc 的返回值为 void*,接受时必须强转,new不需要
ListNode* removeElements(ListNode* head, int val) {
//创建一个虚拟头节点指向头节点:
ListNode* virtualhead = new ListNode(0);
virtualhead ->next = head;
ListNode* cur = virtualhead;
//进行遍历删除操作:
while ( cur -> next != NULL){
if(cur->next->val == val){
ListNode* temp = cur->next;
cur->next = cur->next->next;
delete temp;
}
else{
cur = cur-> next;
}
}
return virtualhead->next;
delete virtualhead;
// head = dummyHead->next;
// delete dummyHead;
// return head;
时间复杂度: O(n)
空间复杂度: O(1)
707.设计链表
题目链接: link
这是一道对于链表设计的一道非常综和全面的题目
对于全部要进行操作的链表过程,首先便是要先对整个链表的结构体等,以及一些变量数据进行一个统一的定义和声明,然后再分别对内部每个功能进行验证。
在进行插入操作时,链表节点之间的赋值顺序也是非常重要的。
必须要先让新节点指向要分配的位置的正确的下一个节点位置,然后再让虚拟头节点的下一个节点更改为新节点,一旦顺序错误,如先让新节点放在虚拟头节点后面,在去让新节点指向原先虚拟头节点的下个就找不到了.
在寻找第index下标位置时,我们定义了一个临时节点current,这里的current在第一个函数找下标数值时是指向的虚拟头节点的next,因为在while判断中如果Index等于0,那么我们就直接需要这个位置的数值,那便是虚拟头节点next原本真正头节点的位置数值。
但在第index前插入新节点的时候,我们就又把current定义为虚拟头结点的位置,因为拿特殊情况如此时链表只有一个元素等特殊情况时,我们便是把新节点插入虚拟头节点之后,那么以current来进行这些操作时,便是current代替虚拟头结点的位置。
(代码随想录答案)
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;
}
return cur->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 = cur->next;
}
cur->next = newNode;
_size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index > _size) return;
if(index < 0) index = 0;
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
//如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
//如果之后的程序不小心使用了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;
};
(自写版本)——
要先对成员变量进行定义!
在addAtTail函数中,最后current -> next = newNode;,而不是newNode = current-> next;因为我们是想要在这个位置处添加一个新节点。如果是后面的写法,代表着更改新节点的值为当前cur->next的。两个意思不一样。
此外存在一个关于C++基本定义的语法问题。
在全文中,我们要先对成员变量进行定义,即为
private:
int _size;
LinkedNode* _dummyHead;
然后再对这些变量进行处理操作,否则将是无定义的成员变量。
然后再在里面进行初始化操作。
注意在变量定义时候如果没分配空间,和初始化内的写法结合来看,不能写成:
ListNode* virtualhead;
ListNode* virtualhead = new ListNode(0);的重复定义,导致字节位数错乱。
可以是:
ListNode* virtualhead = new ListNode(0);
ListNode* virtualhead = new ListNode(0);
或
ListNode* virtualhead;
virtualhead = new ListNode(0);
class MyLinkedList {
private: //在做题目前给出一些基本定义在私域内
//首先定义链表结构体: (这里结构体定义按照前面力扣在移除链表一题给的定义方法定义
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) {} //两个参数
};
//定义虚拟头节点,后面都用虚拟头节点进行操作:
//ListNode* virtualhead = new ListNode(0);
ListNode* virtualhead;
int length;
public:
//根据示例解释,这里的作用是用于初始化对象 ;为构造函数
MyLinkedList() {
//初始化对象,即构造定义的一个虚拟头节点
virtualhead = new ListNode(0);
//ListNode* virtualhead = new ListNode(0);
length = 0; //链表长度初始化为0
}
//获取下标index处的val;
int get(int index) {
if(index < 0 || index > (length -1)) {//不在链表范围以内,下标无效
return -1;
}
else {
ListNode* current = virtualhead -> next; //定义链表真正的头节点
while(index--){ //沿着链表向后指向Index次 ;如果就一个元素那么index = 0;直接跳出while返回当前值
current = current ->next;
}
return current -> val;
}
}
void addAtHead(int val) {
if(val < 0 || val > 1000) {//超出题目有效数据范围
return ;
}
//创建一个新的节点,分配空间,内部数值为val
ListNode* newNode = new ListNode(val);
//这里必须要先让新节点指向要分配的位置的正确的下一个节点位置,然后再让虚拟头节点的下一个节点更改为新节点,一旦顺序错误,如先让新节点放在虚拟头节点后面,在去让新节点指向原先虚拟头节点的下个就找不到了
newNode -> next = virtualhead ->next;
virtualhead -> next = newNode;
length ++; //链表长度增加
}
void addAtTail(int val) {
if(val < 0 || val > 1000) {//超出题目有效数据范围
return ;
}
//创建一个新的节点,分配空间,内部数值为val
ListNode* newNode = new ListNode(val);
ListNode* current = virtualhead ; //用于遍历整个链表,这里不定义为virtual的next因为在下面while判定时候有可能初始下就是空的,第一个就是空链表,就是直接插入新节点。
while(current-> next != nullptr){
current = current ->next;
}
current -> next = newNode;
//newNode = current-> next;
//newNode -> next = NULL;
length ++;
}
void addAtIndex(int index, int val) {
if(val < 0 || val > 1000) {//超出题目有效数据范围
return ;
}
if(index > length ) {//不在链表范围以内,下标无效
return ;
}
if(index < 0) index = 0;
ListNode* newNode = new ListNode(val);
ListNode* current = virtualhead ;
while(index--){
current = current-> next;
}
newNode ->next = current->next;
current->next = newNode;
length ++;
}
//移除节点
void deleteAtIndex(int index) {
// if(val < 0 || val > 1000) {//超出题目有效数据范围
// return ;
// }
if(index < 0 || index >= length ) {//不在链表范围以内,下标无效
return ;
}
ListNode* current = virtualhead;
ListNode* temp;
while(index--){
current = current->next;
}
temp = current ->next;
current ->next= current ->next ->next;
delete temp;
temp = nullptr;
length--;
}
206.反转链表(常考基础数据结构考察题目)
经典的递归思想考察题目
题目链接: link
双指针解法
整个思路便是两个指针来存取并不断移动翻转的过程。
要注意的易错点便是用temp去临时存取下一个节点信息的时候要最优先,然后注意每个直接赋值变化的替代关系和顺序。
ListNode* fast = head;
ListNode* slow = nullptr;
ListNode* temp;
while(fast !=0){
temp = fast->next;
fast ->next = slow;
slow = fast;
fast = temp;
}
return slow;
时间复杂度: O(n)
空间复杂度: O(1)
递归方法:
递归三要素:
第一步:明确函数干什么
第二步:找出基线条件
第三步:找出递归条件
——》
第一步:定义一个反转单链表的函数,参数为链表的第一个节点
第二步:找出基线条件,当链表为空或者链表只有一个节点,返回链表本身即可
第三步:问题拆解
如果一个问题 A 可以分解为若干子问题 B、C、D,你可以假设子问题 B、C、D 已经解决,在此基础上思考如何解决问题 A
//递归法就是把中间不断重复的一个替代交换过程给归纳整合起来,让其不断地去重复,简洁化代码,替代原先的while等循环条件
//递归法:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// ListNode *fast =head;
// ListNode *slow = nullptr;
// ListNode *temp;
return reverse(head,nullptr);
}
ListNode* reverse(ListNode* fast, ListNode* slow){
if(fast == nullptr) return slow;
ListNode *temp = fast->next;
fast -> next =slow;
//slow = fast;
//fast = temp;
return reverse(temp, fast);
}
};
时间复杂度: O(n), 要递归处理链表的每个节点
空间复杂度: O(n), 递归调用了 n 层栈空间
上诉递归只是正对双指针方法的一个简化,
另一种递归思想便是找到最后一个链表节点,由此节点一个个不断翻转,往回推进的一个过程:
(具体解释如下)
一个比较清晰的讲解链接: link
前面的判定是递归的出口,假设五个节点为例,那么内部就有五个reverselift等待后续处理,在第五个返回时,head为5,然后倒回去把前面reverselist内部原先没有处理的后续代码依次再处理掉。(因此与栈相似)
/**
* 以链表1->2->3->4->5举例
* @param head
* @return
*/
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
/*
直到当前节点的下一个节点为空时返回当前节点
由于5没有下一个节点了,所以此处返回节点5
*/
return head;
}
//递归传入下一个节点,目的是为了到达最后一个节点
ListNode newHead = reverseList(head.next);
/*
第一轮出栈,head为5,head.next为空,返回5
第二轮出栈,head为4,head.next为5,执行head.next.next=head也就是5.next=4,
把当前节点的子节点的子节点指向当前节点
此时链表为1->2->3->4<->5,由于4与5互相指向,所以此处要断开4.next=null
此时链表为1->2->3->4<-5
返回节点5
第三轮出栈,head为3,head.next为4,执行head.next.next=head也就是4.next=3,
此时链表为1->2->3<->4<-5,由于3与4互相指向,所以此处要断开3.next=null
此时链表为1->2->3<-4<-5
返回节点5
第四轮出栈,head为2,head.next为3,执行head.next.next=head也就是3.next=2,
此时链表为1->2<->3<-4<-5,由于2与3互相指向,所以此处要断开2.next=null
此时链表为1->2<-3<-4<-5
返回节点5
第五轮出栈,head为1,head.next为2,执行head.next.next=head也就是2.next=1,
此时链表为1<->2<-3<-4<-5,由于1与2互相指向,所以此处要断开1.next=null
此时链表为1<-2<-3<-4<-5
返回节点5
出栈完成,最终头节点5->4->3->2->1
*/
head.next.next = head;
head.next = null;
return newHead;
}