代码随想录Day3|203.移除链表元素 (虚拟头节点)707.设计链表 206反转链表

头指针,头节点

  • 头指针是一个引用,它指向链表的开始位置。
  • 头节点是链表中的实际节点,它可能是存储数据的第一个节点,也可能是一个不存储数据的哑节点。
    从头节点开始遍历链表,直到到达所需的位置。以下是几种常见的访问链表元素的方法:
  1. 遍历访问:从头节点开始,逐个遍历链表直到到达目标下标的位置。这种方法的时间复杂度是O(n),其中n是链表的长度。
  2. 递归访问:使用递归函数遍历链表,每次递归调用时,下标减1,直到下标为0时,返回当前节点。
  3. 使用索引映射:如果需要频繁地通过下标访问链表,可以创建一个额外的数据结构(如数组或哈希表),将链表的每个节点映射到一个下标上。这样可以通过下标快速访问链表节点,但这会增加额外的空间复杂度。
  4. 双向链表:如果链表是双向的,即每个节点有两个指针,一个指向下一个节点,一个指向上一个节点,那么可以通过当前节点快速地访问前一个或后一个节点。
  5. 跳表:跳表是一种允许快速查找的数据结构,它是链表的一种改进。通过在链表中增加多级索引,可以加快查找速度,但会增加空间复杂度。

203.移除链表元素 (虚拟头节点)

识别

这段代码是一个链表操作的函数,其目的是移除链表中所有值为 val 的节点。函数接收两个参数:一个链表的头节点 head 和一个整数值 val。函数返回一个指向修改后链表头节点的指针。

核心/易错

核心逻辑是遍历链表,检查每个节点的值是否等于 val,如果是,则将该节点从链表中移除。易错点在于处理链表节点的删除操作时,需要确保不会丢失对下一个节点的引用,同时要正确地处理边界情况,如头节点就是需要删除的节点。

难点/亮点

难点在于如何不使用额外空间来处理链表节点的删除,同时保持链表的完整性。亮点是使用了一个哑节点(dummy node)dummyhead 来简化头节点处理的逻辑,这样不需要单独处理头节点可能被删除的情况。

算法设计思路

  1. 创建一个哑节点 dummyhead,将其 next 指针指向头节点 head。这样可以统一处理所有节点,包括头节点。
  2. 使用一个指针 cur 指向哑节点,遍历链表。
  3. 在遍历过程中,检查 cur->next 指向的节点是否需要被删除(即节点值等于 val)。
  4. 如果需要删除,就将 cur->next 指向下一个节点,实现删除操作。
  5. 如果不需要删除,就将 cur 指针移动到下一个节点。
  6. 重复步骤3-5,直到 cur->nextNULL,即到达链表末尾。
  7. 返回哑节点的 next 指针,即新链表的头节点。

代码实现

struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode dummyhead; // 创建哑节点
    dummyhead.next = head;      // 哑节点指向头节点
    struct ListNode* cur = &dummyhead; // 创建指针cur指向哑节点

    while (cur->next != NULL) { // 遍历链表直到末尾
        if (cur->next->val == val) { // 检查当前节点值是否等于val
            cur->next = cur->next->next; // 删除节点
        } else { // 如果当前节点不需要删除
            cur = cur->next; // 移动指针到下一个节点
        }
    }

    return dummyhead.next; // 返回新链表的头节点
}

206反转链表

在C或C++语言中,dummyhead.next = head;dummyhead->next = head; 都是用来给指针所指向的结构体或对象的成员赋值的语句,但是它们使用的场景略有不同。

  1. dummyhead.next = head; 这种写法适用于 dummyhead 是一个对象或者结构体的实例,而不是指针。这里 dummyhead 直接访问其 next 成员,并将 head 的值赋给它。
  2. dummyhead->next = head; 这种写法适用于 dummyhead 是一个指针,指向一个对象或结构体。这里 dummyhead 通过箭头操作符(->)来访问指针指向的对象的 next 成员,并将 head 的值赋给它。
  3. 使用.操作符时,dummyhead 必须是对象本身;使用->操作符时,dummyhead 必须是指向对象的指针。如果 dummyhead 是指针,应该使用 ->;如果不是指针,而是对象本身,应该使用 .

识别

这段代码是用C语言编写的,实现了一个单链表的反转功能。代码定义了一个结构体ListNode,用于表示链表的节点,每个节点包含一个整数值val和一个指向下一个节点的指针next。函数reverseList接收一个链表的头节点head作为参数,返回一个新的头节点,该节点是原链表反转后的结果。

核心/易错

核心部分是循环中的节点反转逻辑,这是实现链表反转的关键。易错点在于指针的操作,特别是对cur->nextprecur指针的更新,如果操作不当,可能会导致内存泄漏或者程序崩溃。

难点/亮点

难点在于理解如何在不使用额外空间的情况下,通过调整指针的指向来实现链表的反转。亮点是该算法的时间复杂度为O(n),其中n是链表的长度,这是因为算法只需要遍历一次链表即可完成反转。

算法设计思路

  1. 初始化三个指针:cur指向当前遍历到的节点,pre指向已经反转部分的最后一个节点(初始时为NULL),temp用于临时存储下一个节点。
  2. 遍历链表,对于每个节点,先将cur->next指向pre,实现反转。
  3. 更新precur指针,pre前移至当前节点,cur前移至下一个待处理的节点。
  4. 重复步骤2和3,直到cur为NULL,即遍历完整个链表。
  5. 此时pre指向了新的头节点,返回pre

代码实现

#include <stdlib.h>
struct ListNode {
    int val;
    struct ListNode *next;
};

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode *cur = head;
    struct ListNode *pre = NULL;
    struct ListNode *temp = NULL;
    
    while (cur != NULL) {
        temp = cur->next; // 保存当前节点的下一个节点
        cur->next = pre;  // 将当前节点的 next 指向前一个节点
        pre = cur;        // 将 pre 指针前移
        cur = temp;       // 将 cur 指针前移
    }
    
    return pre; // 此时 pre 指向新的头节点
}

707.设计链表

在设计单向链表时

  1. 节点初始化:每个节点通常包含至少两个属性:存储数据的 val 和指向下一个节点的指针 next。对于双向链表,还需要一个指向前一个节点的指针 prev

  2. 链表的下标:链表的下标通常从0开始,即第一个节点的下标为0,以此类推。

  3. 头节点和尾节点:在单链表中,头节点通常是一个特殊的节点,它的 val 属性不存储有效数据,它的 next 指向链表的第一个有效节点。尾节点的 next 指向 NULL,表示链表的结束。

4 初始化代码:在C语言中,节点可以通过 malloc 来动态分配内存,然后初始化 valnext(对于双向链表还有 prev)。

  1. 错误处理:在 get 方法中,如果索引无效,应该返回 -1

  2. 内存管理:在链表不再使用时,应该释放所有节点的内存,以避免内存泄漏。

结构体定义易错

别名MyLinkedList是在结构体定义结束后才可以识别的一个别名故在结构体里面,是不认识这个别名而报错。

#include <stdlib.h>

// 定义单链表的节点结构体

typedef struct MyLinkedList {

    int val;

    struct MyLinkedList *next;

} MyLinkedList;

// 创建一个新的节点

MyLinkedList* createNode(int val) {

    MyLinkedList* newNode = (MyLinkedList*)malloc(sizeof(MyLinkedList));

    if (newNode == NULL) {

        // 处理内存分配失败的情况

        return NULL;

    }

    newNode->val = val;

    newNode->next = NULL;

    return newNode;

}

// MyLinkedList 类的实现(而`MyLinkedList`是一个自定义的类,用于模拟链表的行为。在这个问题中,你需要实现一个支持动态数据存储和访问的链表,而不是使用现成的数组或链表数据结构。)
typedef struct {
    MyLinkedList *head; // 指针,指向链表的头节点
    int size;           // 整数,记录链表的当前大小
} MyLinkedList;

// 初始化 MyLinkedList 对象
MyLinkedList* myLinkedListCreate() {
    MyLinkedList* obj = (MyLinkedList*)malloc(sizeof(MyLinkedList)); // 动态分配内存,创建一个新的 MyLinkedList 对象
    if (obj == NULL) {
        // 处理内存分配失败的情况
        return NULL; // 如果内存分配失败,返回 NULL
    }
    obj->head = createNode(0); // 创建一个头节点,并赋值给 obj 的 head 成员。注意:createNode 函数未在代码中定义,应是预先定义的函数。
    obj->size = 0;            // 初始化链表大小为 0
    return obj;               // 返回新创建的链表对象
}

// 获取链表中下标为 index 的节点的值
int myLinkedListGet(MyLinkedList* obj, int index) {
    // 检查索引是否有效,如果索引小于0或者大于等于链表的尺寸,则返回-1
    if (index < 0 || index >= obj->size) {
        return -1;
    }
    
    // 从头节点的下一个节点开始遍历,即跳过头节点
    MyLinkedList* current = obj->head->next; // 跳过头节点
    
    // 遍历链表直到达到指定的索引位置
    for (int i = 0; i < index; i++) {
        current = current->next; // 移动到下一个节点
    }
    
    // 返回当前节点的值
    return current->val;
}
if (index = node->val) {

        return index;

    } else {

        return -1;

    }
Q:why跳过头节点
在这段代码中,`obj->head` 是链表的头节点,它通常不包含数据,而是作为一个哨兵节点存在。`obj->head->next` 指向链表中的第一个实际存储数据的节点。因此,当我们想要获取链表中下标为 `index` 的节点的值时,我们需要从 `obj->head->next` 开始遍历,这样可以确保我们能够正确地访问到链表中的数据节点。如果链表使用的是单链表结构,那么头节点可能就只是链表的第一个数据节点,这种情况下就不需要跳过头节点,直接从 `obj->head` 开始遍历即可。但在双向链表中,头节点的设计使得我们可以更方便地处理链表的头部操作,而不需要每次插入或删除时都检查链表是否为空。
// 将一个值为 val 的节点插入到链表中第一个元素之前
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {

    MyLinkedList* newNode = createNode(val);

    newNode->next = obj->head->next;

    obj->head->next = newNode;

    obj->size++;

}

// 将一个值为 val 的节点追加到链表中作为链表的最后一个元素

void myLinkedListAddAtTail(MyLinkedList* obj, int val) {//MyLinkedList* newNode = createNode(val); 

//if(obj->next=NULL){

   // obj->next=newNode;

    //newNode->next=NULL;

}

    MyLinkedList* newNode = createNode(val);

    if (obj->head->next == NULL) {

        obj->head->next = newNode;

    } else {

        MyLinkedList* current = obj->head;

        while (current->next != NULL) {

            current = current->next;

        }
       current->next = newNode;

    }
     obj->size++;
}```

Q:if为头节点的
```C
// 将一个值为 val 的节点插入到链表中下标为 index 的节点之前
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
    // 检查索引是否有效
    if (index > obj->size || index < 0) {
        return; // 索引超出链表范围或为负数,直接返回
    }
    // 创建一个新的节点,值为val
    ListNode* newNode = createNode(val);()
    // 如果插入位置为0,即链表头部
    if (index == 0) {
        newNode->next = obj->head->next; // 新节点的下一个节点是头节点的下一个节点
        newNode->prev = obj->head; // 新节点的前一个节点是头节点
        if (obj->head->next != NULL) { // 如果头节点的下一个节点不为空
            obj->head->next->prev = newNode; // 更新头节点的下一个节点的前一个节点为新节点
        }
        obj->head->next = newNode; // 头节点的下一个节点变为新节点
    } else {
        // 否则,遍历链表找到插入位置的前一个节点
        ListNode* current = obj->head->next;
        for (int i = 0; i < index - 1; i++) {
            current = current->next;
        }
        // 插入新节点
        newNode->next = current->next; // 新节点的下一个节点是当前节点的下一个节点
        newNode->prev = current; // 新节点的前一个节点是当前节点
        if (current->next != NULL) { // 如果当前节点的下一个节点不为空
            current->next->prev = newNode; // 更新当前节点的下一个节点的前一个节点为新节点
        }
        current->next = newNode; // 当前节点的下一个节点变为新节点
    }
    // 更新链表的大小
    obj->size++;
}
// 删除链表中下标为 index 的节点
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    if (index < 0 || index >= obj->size) {
        return; // 无效的索引
    }

    MyLinkedList* current = obj->head;
    if (index == 0) {
        obj->head->next = current->next->next;
        free(current->next);
    } else {
        for (int i = 0; i < index - 1; i++) {
            current = current->next;
        }
        MyLinkedList* temp = current->next;
        current->next = temp->next;
        free(temp);
    }
    obj->size--;
}

// 释放链表内存
void myLinkedListFree(MyLinkedList* obj) {
    MyLinkedList* current = obj->head;
    while (current != NULL) {
        MyLinkedList* temp = current;
        current = current->next;
        free(temp);
    }
    free(obj);
}```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值