链表
C++结构体与类的区别,结构体中的默认所有成员都是公有的,类中默认所有成员都是私有的。但是两者都可以设置public, private, protected 成员变量,成员函数,也都可以设置构造函数,析构函数,并且都有继承和多态特性。C++中类和结构体基本一致。
2021/7/5 先复习类的构造函数,析构函数,多态等,复习完之后再上链表。
明天早上先用C++代码构造一个链表再说,然后对于删除元素部分,申请内存,删除内存的操作要贯通熟悉一下
特殊点:要多用const指明该变量不可被更改,以及函数入口参数使用引用而不是传值。
链表构造:
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* createList(const vector<int>& ListData) { // 以后要多传引用,用const指定这是个不可更改的量
if (ListData.empty()) {
return nullptr;
}//检测传入数组是否为空
ListNode* head = new ListNode{ ListData[0],nullptr };//在堆区存储申请内存存储头结点
ListNode* current = head; //常规区域定义一个指针变量
for (int i = 1; i < ListData.size(); i++) {
current->next = new ListNode(ListData[i], nullptr);
current = current->next;
}
return head;
}
/*这里有一个细节就是delete之后head变成了悬空指针,之后立刻使用head = newhead; 对悬空指针进行了确定,直到head变成了nullptr;
最终所有的悬空指针都得到了处理.*/
void freeList(ListNode* head) {
while (head != nullptr) {
ListNode* newhead = head->next;//常规区域定义一个指针变量
delete head; 这里释放了 head 指向的堆区内存,但 head 变量本身仍然存在,只是它现在是悬空指针(dangling pointer)
head = newhead; //给悬空指针重新赋值
}
}
int main() {
//首先得构造一个链表
vector<int> ListData{1,2,6,3,4,5,6 };
ListNode* head = createList(ListData);
ListNode* current = head;
while (current!= nullptr) {
cout << current->val << endl;
current = current->next;
}
freeList(head);
return 0;
}
移除链表元素:按照代码随想录的思路自己写
我的代码:
ListNode* removeElements(ListNode* head, int val) {
//方法1,不添加虚拟头节点.要注意对于释放节点的删除
/*ListNode* deleteNode = nullptr;
ListNode* searchNode = nullptr;
ListNode* prevNode = nullptr;
while (head != nullptr) {
if (head->val == val) {
deleteNode = head;
head = head->next;
delete deleteNode;
deleteNode = nullptr;
}
else {
break;
}
}//这一部分解决的是头节点出现要删除的值的时候
if (head == nullptr) {
return head;
}
searchNode = head->next;
prevNode = head;
while (searchNode != nullptr) {
if (searchNode->val == val) {
prevNode->next = searchNode->next;
deleteNode = searchNode;
searchNode = searchNode->next;
delete deleteNode;
deleteNode = nullptr;
}
else {
searchNode = searchNode->next;
prevNode = prevNode->next;
}
}
return head;*/
//方法2,添加虚拟头节点.
ListNode* preHeadNode = new ListNode;
preHeadNode->next = head;
ListNode* searchNode = head;
ListNode* lastNode = preHeadNode;
ListNode* deleteNode = nullptr;
while (searchNode != nullptr) {
if (searchNode->val == val) {
lastNode->next = searchNode->next;
deleteNode = searchNode;
searchNode = searchNode->next;
delete deleteNode;
deleteNode = nullptr;
}
else {
searchNode = searchNode->next;
lastNode = lastNode->next;
}
}
head = preHeadNode->next;
delete preHeadNode;
return head;
}
使用虚拟头结点是最轻松的,不用再顾及若头节点要被删除的结果。
最简代码如下:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* preHeadNode = new ListNode; //构造虚拟头结点
preHeadNode->next = head;//虚拟头节点接到最前端去
ListNode* searchNode = preHeadNode;//从最开始进行索引
ListNode* deleteNode = nullptr;//用于删除结点的堆区内存
while (searchNode->next != nullptr) {
if (searchNode->next->val == val) {
deleteNode = searchNode->next;
searchNode->next = searchNode->next->next;
delete deleteNode;
deleteNode = nullptr;//考虑到悬空指针的问题。
}
else {
searchNode = searchNode->next;
}
}//这一步的方法很简洁,而且关键点是将lastnode于currentnode使用单个变量就解决了,后边可以学习使用这种方式。
head = preHeadNode->next;
delete preHeadNode;
return head;
}
};
设计链表 ;删除链表结点时,一定要注意释放内存,并把悬空指针赋值为空指针;void函数内部可以用return的。
//类内搭建一个链表:
class MyLinkedList {
public:
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val) : val{ val }, next{ nullptr }{}//构造列表初始化
};//在类的内部定义一个链表结点结构体
MyLinkedList() {
dummyHead = new LinkedNode{ 0 };//虚拟头结点
size = 0;//实际链表长度
}
int get(int index) {
if (index > size - 1 || index < 0) {
return -1;
}
LinkedNode* searchNode = dummyHead; //只是定义了一个指针变量,并没有申请堆区内存,不考虑释放的问题
for (int i = 0; i <= index; i++) {
searchNode = searchNode->next;
}
return searchNode->val;
}
void addAtHead(int val) {
LinkedNode* addNode = new LinkedNode{ val };
addNode->next = dummyHead->next;
dummyHead->next = addNode;
size++;
}
void addAtTail(int val) {
LinkedNode* tailNode = new LinkedNode{ val };
LinkedNode* searchNode = dummyHead->next;
while (searchNode->next != nullptr) {
searchNode = searchNode->next;
}
searchNode->next = tailNode;
size++;
}
void addAtIndex(int index, int val) {
if (index >=0 && index <= size) {
LinkedNode* addNode = new LinkedNode{ val };
LinkedNode* searchNode = dummyHead;
for (int i = 0; i < index; i++) {
searchNode = searchNode->next;
}
addNode->next = searchNode->next;
searchNode->next = addNode;
size++;
}
}
void deleteAtIndex(int index) {
if (index >= 0 && index < size)
{
LinkedNode* searchNode = dummyHead;
LinkedNode* deleteNode;
for (int i = 0; i < index; i++)
{
searchNode = searchNode->next;
}
deleteNode = searchNode->next;
searchNode->next = searchNode->next->next;
delete deleteNode;//释放该堆区内存
deleteNode = nullptr; //将悬挂指针赋值为空指针
size--;
}
}
private:
int size;
LinkedNode* dummyHead; //类MyLinkedList内部存储了一个链表,它只需要记住链表头结点就好了。
};
反转链表,重要部分:不要忽略边缘条件判断这一点
代码随想录里用到了递归,今天搞清楚递归调用的内存占用以及数据返回。
//反转链表
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* searchNode = head;
ListNode* temp = nullptr;
while (searchNode != nullptr) { //其实这里就是边缘条件判断了,不要忽略边缘条件判断这一点
temp = searchNode->next;
searchNode->next = prev;
prev = searchNode;
searchNode = temp;
}//最终保留的头结点是prev
return prev;
}
};
递归写法尝试:
//反转链表,使用递归调用来写
class Solution {
public:
ListNode* diguireverse(ListNode* prev, ListNode* searchNode) {
//递归写法
//1.先写边界条件
if (searchNode == nullptr) {
return prev;
}
//2.考虑顺序逻辑
ListNode* temp = searchNode->next;
searchNode->next = prev;
prev = searchNode;
searchNode = temp;
return diguireverse(prev,searchNode);
}//递归先考虑边界条件,再考虑顺序逻辑就比较好写了。
ListNode* reverseList(ListNode* head) {
//ListNode* prev = nullptr;
//ListNode* searchNode = head;
return diguireverse(nullptr, head);
}
};
//使用递归调用写一个阶乘
int jiecheng(int n) {
//第一步:先考虑边界条件,最后那部分
if (n == 0 || n == 1) {
return 1;
}
//第二步考虑顺序逻辑
return n * jiecheng(n - 1);
}
递归写法的步骤顺序:
1.考虑边界条件2.按照顺序写法去写。
如何理解递归调用:
可以去想象调用函数入栈的操作,执行一个函数时,先把相关变量压入栈中,全部执行完之后再把返回值拿出栈。
递归过程中,计算返回值时需要用到另外一个函数,而计算返回值的相关变量还压在栈底,这时需要调用另外一个函数,即继续向栈中压入新函数的变量,就这样不断调用,一直到达边界条件后,才会出现一个准确的返回值,出现在栈顶部分,此时再往回走,之前的函数返回值都可以拿到一个准确的变量,一直到达栈的底部拿到最终的返回值,这个返回值出栈,栈中就不在有东西了,具体示意图绘制如下:
复杂比较难理解的反转链表方法:后续可以多想一下,值得思考。这个想起来确实比较复杂一些。这个递归的形式比较特殊,还是比较开阔眼界的。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
//换一种方法:直接从尾部进行反转,还是用递归
//先写边界条件
if (head == nullptr||head->next==nullptr) {
return head;//来到了末尾
}
ListNode* newhead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newhead;
}
};
两两交换链表中的节点
//我的写法
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyNode = new ListNode();
dummyNode->next = head;
ListNode* cur = dummyNode;
//虚拟头结点搭建好了
ListNode* oneNode;
ListNode* twoNode;
ListNode* thirdNode;
if (head == nullptr) {
return nullptr;
}
if (head->next == nullptr) {
return head;
}
while (cur->next!=nullptr) {//考虑偶数个(>=2个)情况
if (cur->next->next == nullptr) {//考虑奇数个(>=3)情况
break;
}
oneNode = cur->next;
twoNode = cur->next->next;
thirdNode = cur->next->next->next;
cur->next = twoNode;
twoNode->next = oneNode;
oneNode->next = thirdNode;
cur = oneNode;
}
head = dummyNode->next;
delete dummyNode;
return head;
}
//代码随想录中的写法
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
ListNode* cur = dummyHead;
while(cur->next != nullptr && cur->next->next != nullptr) {
ListNode* tmp = cur->next; // 记录临时节点
ListNode* tmp1 = cur->next->next->next; // 记录临时节点
cur->next = cur->next->next; // 步骤一
cur->next->next = tmp; // 步骤二
cur->next->next->next = tmp1; // 步骤三
cur = cur->next->next; // cur移动两位,准备下一轮交换
}
ListNode* result = dummyHead->next;
delete dummyHead;
return result;
}
};
我的写法和代码随想录中c++写法区别:1.不需要单独考虑空链表和只有一个节点的链表,这些在while的判断表达式中已经包括了;2.这里有一个重要的点就是,&&和||都是短路判断,&&左边的表达式为0就不会再计算右边的表达式,||左边的表达式计算为1就不会再计算右边的表达式。这个很重要,对于:
while(cur->next != nullptr && cur->next->next != nullptr)
如果是空链表,左侧计算完就不会再计算右边;如果是单个节点链表,左侧计算为1,右侧计算为0.不会执行while内部表达式,直接返回本身。3.我的代码中的twoNode代表一对节点的右侧,这个节点不用单独拿出来,直接用->next->next操作即可。
顺序写法和递归写法(两两交换链表中的节点)
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyNode = new ListNode();
dummyNode->next = head;
ListNode* cur = dummyNode;
//虚拟头结点搭建好了
ListNode* oneNode;
ListNode* twoNode;
ListNode* thirdNode;
while (cur->next!=nullptr && cur->next->next == nullptr) {//考虑偶数个(>=2个)情况
oneNode = cur->next;
twoNode = cur->next->next;
thirdNode = cur->next->next->next;
cur->next = twoNode;
twoNode->next = oneNode;
oneNode->next = thirdNode;
cur = oneNode;
}
head = dummyNode->next;
delete dummyNode;
return head;
}
ListNode* diguiswapPairs(ListNode* head) {//改成递归算法
//考虑终止条件
if (head == nullptr || head->next == nullptr) {
return head;
}
//按照顺序思路编写
ListNode* dummyNode = new ListNode();
dummyNode->next = head;
ListNode* cur = dummyNode;
//虚拟头结点搭建好了
ListNode* oneNode = cur->next;
ListNode* twoNode = cur->next->next;
ListNode* thirdNode = cur->next->next->next;
cur->next = twoNode;
twoNode->next = oneNode;
oneNode->next = thirdNode;
cur = oneNode;
ListNode* newhead = diguiswapPairs(cur->next);
cur->next = newhead;
head = dummyNode->next;
delete dummyNode;
return head;
}
};
小总结:任何可以通过一定循环操作进行的代码,都可以用递归写法解决(1.写终止条件,2.按照顺序思路编写),以后遇到类似情况都可以用递归思路去写,可以锻炼自己的逻辑思维。
删除链表倒数第n个节点
代码随想录里的思路非常好。双指针先锁定相对距离,然后一步一步滑动前进,非常简洁漂亮的思路。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyNode = new ListNode();
dummyNode->next = head;
ListNode* fastNode = head;//fastnode走n步之后为nullptr,那么走之前的位置就是要删除的节点
ListNode* slowNode = dummyNode;
ListNode* deleteNode;
while (n-- && fastNode != nullptr) {
fastNode = fastNode->next;
};
while (fastNode != nullptr) {
fastNode = fastNode->next;
slowNode = slowNode->next;
}
//循环出来后fastNode就等于nullptr了
deleteNode = slowNode->next;
slowNode->next = slowNode->next->next;
delete deleteNode;
head = dummyNode->next;
delete dummyNode;
return head;
}
};
链表相交
代码随想录的思路非常好。代码随想录中用到了swap函数,应该是STL中的,这一部分需要后期加深。
补充:C++中swap函数的用法。swap是STL库中的交换两个变量的函数的,输入变量是引用形式输入。
class Solution {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
//代码随想录中的思路太妙了
ListNode* curA = headA;
ListNode* curB = headB;
int aLength=0, bLength = 0;
while (curA != nullptr) {
aLength++;
curA = curA->next;
}
while (curB != nullptr) {
bLength++;
curB = curB->next;
}
curA = headA;
curB = headB;
if (aLength >= bLength) {
int space = aLength - bLength;
while (space--) {
curA = curA->next;
}
while (curA != nullptr) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
}
else {
int space = bLength - aLength;
while (space--) {
curB = curB->next;
}
while (curB != nullptr) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
}
return nullptr;
}
};
环形链表
快慢指针法,这道题就比较有难度了,需要认真分析问题并将问题简化才能编出精简的代码。
圆环问题的思路值得反复回味。
//链表相交
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
//用快慢指针法
ListNode* fast = head;
ListNode* slow = head;
while (fast!=nullptr&&fast->next!=nullptr) {//防止没有环的情况,以及只有空链表和单个链表的情况,这个思路还是挺缜密的
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
{
ListNode* index1 = head;
ListNode* index2 = fast;
while (index1 != index2)
{
index1 = index1->next;
index2 = index2->next;
}
return index1;
}
}
return nullptr;
}
};