LeetCode203 移除链表元素
题目链接:203.移除元素
这个题很简单,有两种不同的做法,最好的一种就是使用虚拟头结点(也称为哨兵节点),如果没有虚拟头结点,设置的遍历指针curp,就必须分为 是头结点 和 不是头结点两种,不太方便简洁。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode *new;//虚拟头结点
new = (struct ListNode *)malloc(sizeof(struct ListNode));
new->next = head;
struct ListNode *p = new;
while(p->next != NULL){
if(p->next->val == val){
p->next = p->next->next;
}
else{
p = p->next;
}
}
head = new->next;
free(new);//建议养成手动清理内存的习惯
return head;
}
这样通过设置一个虚拟头结点,原链表的所有节点就都可以按照统一的方式进行移除了。
还有一点需要注意的就是养成手动清理内存的习惯,只要是申请空间的指针,用完之后都要释放
掉。
LeetCode707 设计链表
题目链接:LeetCode707 设计链表
刚看这道题的时候,因为数据结构课已经学完链表的构建和各种操作了,以为会很快的AC,但事实证明,并不是如此,这道题快花了我一下午的时间才彻底掌握弄懂。
为什么会花费这么长时间呢?大体代码好些,一是LeetCode的一个缺点,只给要实现代码的部分,其他部分会隐藏,这样就导致会出现不确定的错误,其实在这个题目中,LeetCode已经给你封装好了一个ListNode结构体;二是我自己从来没接触到过链表还有下标的情况,再结合跟带空头结点的链表一起用,在边界方面(index == 0),处理的不太好。
其实这个题目就是想考察我们对带头节点的链表的掌握(因为题目本身在隐藏部分就定义了一个ListNode函数,里面有val值,有next指针,也是我在解题过程中发现的)。我感觉本题最重要的就是有下标时对带头结点链表的处理。
typedef struct ListNode ListNode;//定义一下更方便
struct MyLinkedList
{
int length;
ListNode *head;
};
typedef struct MyLinkedList MyLinkedList;//附加头结点的定义
MyLinkedList* myLinkedListCreate()
{
MyLinkedList *obj;
obj = (MyLinkedList *)malloc(sizeof(MyLinkedList));
obj->length = 0;
obj->head = NULL;//初始化
return obj;
}
int myLinkedListGet(MyLinkedList* obj, int index)
{
//排除index无效的情况
if(index >= obj->length || index < 0)
return -1;
ListNode *curp = obj->head;//curp指针代表链表下表为0的节点
while(index--)
{
curp = curp->next;
}
return curp->val;
}
void myLinkedListAddAtHead(MyLinkedList* obj, int val)
{
ListNode *temp;
temp = (ListNode *)malloc(sizeof(ListNode));
temp->val = val;
temp->next = obj->head;
obj->head = temp;
obj->length++;
}
void myLinkedListAddAtTail(MyLinkedList* obj, int val)
{
ListNode *temp;
temp = (ListNode *)malloc(sizeof(ListNode));
temp->val = val;
temp->next = NULL;
obj->length++;
//必须将链表为空的情况单独判断
if(obj->head == NULL)
{
obj->head = temp;
return;
}
ListNode *curp = obj->head;
while(curp->next != NULL)//否则这里会少一个对下标为0的节点(头结点之后的节点)的判断
{
curp = curp->next;
}
curp->next = temp;
}
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val)
{
//链表为空和下标长度无效的情况
if(index > obj->length || index < 0) return;
ListNode *temp;
temp = (ListNode *)malloc(sizeof(ListNode));
temp->val = val;
obj->length++;
//由于是插入在此下标之前,所以应该找前一个下标的元素
//当index为0时,直接将其插入附加头结点的后面
if(index == 0)
{
temp->next = obj->head;
obj->head = temp;
return;
}
ListNode *curp = obj->head;
//由于是插入在此下标之前,所以应该找前一个下标的元素
index -= 1;
while(index--)
{
curp = curp->next;
}
temp->next = curp->next;
curp->next = temp;
}
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index)
{
//index无效的情况(已经包含链表为空的情况)
if(index < 0 || index >= obj->length)
return;
//特殊情况index == 0
if(index == 0)
{
obj->head = obj->head->next;
obj->length--;
return;
}
ListNode *curp = obj->head;
//删除此下标元素,先要找到此下表之前的元素,才能连接起来
index -= 1;
while(index--)
{
curp = curp->next;
}
curp->next = curp->next->next;
obj->length--;
}
void myLinkedListFree(MyLinkedList* obj)
{
if(obj->head == NULL)
{
free(obj);
return;
}
ListNode *p = obj->head, *temp;
while(p != NULL)
{
temp = p->next;
free(p);
p = temp;
}
free(obj);
}
总的来说,因为有了附加头结点,所以在对于有效链表的第一个节点,也就是此题中的下标为0的节点的处理,跟其他节点有些不同,这就是在函数中有许多index == 0 之类的判断语句的原因,当要处理节点是第一个有效链表节点时,要特殊处理。
LeetCode206 反转链表
题目链接:206.反转链表
这道题是唯一一个我以前做过一模一样的题,而且也整理过比较分析过,但是仍然在第一眼想不起来怎么做的题。(这一次我一定要更认真地分析,彻底学懂!掌握!)
反转链表之前我整理过两种方法,大同小异,下面给大家呈现以下:(注意,这两种解法中的链表默认有虚拟头结点)
//方法一:
void ListReverse_L(LinkList &L)
{
LNode *p,*curPtr;
if(L->next&&L->next->next)//判断L中有几个有效数据(>2)
{
p=L->next->next;//记录第二个有效节点的地址
L->next->next=NULL;//将第一个有效节点next指向NULL(把本来的第一个数变成最后一个数)
while(p)
{
curPtr=L->next;//第一个有效节点的地址
L->next=p;//让头结点指向第二个节点
p=p->next;//p现在指向第三个节点
L->next->next=curPtr;//让第二个节点的next指向第一个节点
//经过这个循环,头节点指向第二个节点,第二个节点next指向原来的第一个节点,原来的第一个节点的next指向NULL
//在后面的循环中,curPtr一直代表着头节点的next原来所指向的地址
//第二行就让头结点指向的变为最新的p,L->next->next就代表着新节点的指向,刚才说过curPtr一直只想原来的头节点的next指向的地址
//最后一行就直接让最新的节点的next指向原来头节点指向的节点
}
}
}
//方法二:
void ListReverse_L(LinkList &L) {
if(L->next == NULL) return;
else{
LNode *current = L->next; //当前要反转的节点
LNode *pre = NULL; //当前节点的前一个结点
LNode *next_node = current->next;//当前节点的后一个节点
while(current != NULL) {
next_node = current->next;//用来记录当前节点的后一个节点
current->next = pre;
pre = current;
current = next_node;
}
L->next = pre;
}
}
在听Carl哥讲之后,说实话有很大收获,以前只是看得懂但是现在脑子里有方法的思路概念了,双指针法和递归法。然后再重新回顾以前整理的两种方法,才发现,“方法二”用的就是双指针法
接下来是双指针的思路:
设置两个指针pre 和 current,一个指向当前节点,一个指向前一个节点(也就是current反向后指向的节点)。接下来是关键,就是如何再反转之后还能知道current反转前的后一个节点地址呢,这时就需要另外一个临时指针next_node,来实时获取后一个节点,使链表正常遍历。
下面是这个题的双指针的AC代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head)
{
if(head){
struct ListNode *curp = head;
struct ListNode *prev = NULL;//初始化为NULL
while(curp){
struct ListNode *temp = curp->next;
curp->next = prev;
prev = curp;
curp = temp;
}
head = prev;
}
return head;
}
知道基本解题方法和思路之后很简单, 有一点就是关于初始化的问题,由于反转过来之后,原来的头结点的next应该指向NULL,所以prev初始化为空。用临时指针temp记录当前节点的后一个结点的地址。循环条件到 curp 为空结束,也就是最后一个节点的next处,此时prev指针刚好指向最后一个节点,也就是反转后链表的头结点。
下面介绍递归法的思路:
其实根本思路跟双指针法没有区别,只是在实现方法上,使用了递归的方法:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverse_list(struct ListNode* curp, struct ListNode* prev){
if(curp == NULL)
return prev;
else{
struct ListNode *temp = curp->next;
curp->next = prev;
prev = curp;
curp = temp;
return reverse_list(curp, prev);
}
}
struct ListNode* reverseList(struct ListNode* head)
{
return reverse_list(head, NULL);
}
这两种方法,思路相同,但是实现思路的方法不同,本质上都是使用了两个指针来储存当前反转节点,当前节点前的一个节点,通过再设置一个临时指针,来实现对链表的反转。
但是让我们来看一下我以前整理的“方法一 ”的代码,它是否与双指针法的思路有所不同?
实际从根本思想上来看,仍然是是用双指针法,记录当前反转节点和当前节点的前一个节点,只是这个方法把原本双指针法当前节点,直接赋给虚拟头结点的next,这样就解放了curp指针可以直接更新为curp的下一个,而由于此时L的next(即虚拟的头结点的next已经指向第二个有效值),然后将第二个有效节点和第一个有效节点连接起来(L->next->next=curPtr;)。
在这里我考虑过一个问题,为什么p = p->next不能放在后面?这是因为,在L->next->next=curPtr改变的世界上是第二个有效节点的next指向,如果放在后面,当执行完这一句后,第三个有效节点就不再跟第二个有效节点连接。