代码随想录训练营第三天|LeetCode203、LeetCode707、LeetCode206

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指向,如果放在后面,当执行完这一句后,第三个有效节点就不再跟第二个有效节点连接。

  • 23
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值