头指针,头节点
- 头指针是一个引用,它指向链表的开始位置。
- 头节点是链表中的实际节点,它可能是存储数据的第一个节点,也可能是一个不存储数据的哑节点。
从头节点开始遍历链表,直到到达所需的位置。以下是几种常见的访问链表元素的方法:
- 遍历访问:从头节点开始,逐个遍历链表直到到达目标下标的位置。这种方法的时间复杂度是O(n),其中n是链表的长度。
- 递归访问:使用递归函数遍历链表,每次递归调用时,下标减1,直到下标为0时,返回当前节点。
- 使用索引映射:如果需要频繁地通过下标访问链表,可以创建一个额外的数据结构(如数组或哈希表),将链表的每个节点映射到一个下标上。这样可以通过下标快速访问链表节点,但这会增加额外的空间复杂度。
- 双向链表:如果链表是双向的,即每个节点有两个指针,一个指向下一个节点,一个指向上一个节点,那么可以通过当前节点快速地访问前一个或后一个节点。
- 跳表:跳表是一种允许快速查找的数据结构,它是链表的一种改进。通过在链表中增加多级索引,可以加快查找速度,但会增加空间复杂度。
203.移除链表元素 (虚拟头节点)
识别
这段代码是一个链表操作的函数,其目的是移除链表中所有值为 val
的节点。函数接收两个参数:一个链表的头节点 head
和一个整数值 val
。函数返回一个指向修改后链表头节点的指针。
核心/易错
核心逻辑是遍历链表,检查每个节点的值是否等于 val
,如果是,则将该节点从链表中移除。易错点在于处理链表节点的删除操作时,需要确保不会丢失对下一个节点的引用,同时要正确地处理边界情况,如头节点就是需要删除的节点。
难点/亮点
难点在于如何不使用额外空间来处理链表节点的删除,同时保持链表的完整性。亮点是使用了一个哑节点(dummy node)dummyhead
来简化头节点处理的逻辑,这样不需要单独处理头节点可能被删除的情况。
算法设计思路
- 创建一个哑节点
dummyhead
,将其next
指针指向头节点head
。这样可以统一处理所有节点,包括头节点。 - 使用一个指针
cur
指向哑节点,遍历链表。 - 在遍历过程中,检查
cur->next
指向的节点是否需要被删除(即节点值等于val
)。 - 如果需要删除,就将
cur->next
指向下一个节点,实现删除操作。 - 如果不需要删除,就将
cur
指针移动到下一个节点。 - 重复步骤3-5,直到
cur->next
为NULL
,即到达链表末尾。 - 返回哑节点的
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;
都是用来给指针所指向的结构体或对象的成员赋值的语句,但是它们使用的场景略有不同。
dummyhead.next = head;
这种写法适用于dummyhead
是一个对象或者结构体的实例,而不是指针。这里dummyhead
直接访问其next
成员,并将head
的值赋给它。dummyhead->next = head;
这种写法适用于dummyhead
是一个指针,指向一个对象或结构体。这里dummyhead
通过箭头操作符(->)来访问指针指向的对象的next
成员,并将head
的值赋给它。- 使用
.
操作符时,dummyhead
必须是对象本身;使用->
操作符时,dummyhead
必须是指向对象的指针。如果dummyhead
是指针,应该使用->
;如果不是指针,而是对象本身,应该使用.
。
识别
这段代码是用C语言编写的,实现了一个单链表的反转功能。代码定义了一个结构体ListNode
,用于表示链表的节点,每个节点包含一个整数值val
和一个指向下一个节点的指针next
。函数reverseList
接收一个链表的头节点head
作为参数,返回一个新的头节点,该节点是原链表反转后的结果。
核心/易错
核心部分是循环中的节点反转逻辑,这是实现链表反转的关键。易错点在于指针的操作,特别是对cur->next
、pre
和cur
指针的更新,如果操作不当,可能会导致内存泄漏或者程序崩溃。
难点/亮点
难点在于理解如何在不使用额外空间的情况下,通过调整指针的指向来实现链表的反转。亮点是该算法的时间复杂度为O(n),其中n是链表的长度,这是因为算法只需要遍历一次链表即可完成反转。
算法设计思路
- 初始化三个指针:
cur
指向当前遍历到的节点,pre
指向已经反转部分的最后一个节点(初始时为NULL),temp
用于临时存储下一个节点。 - 遍历链表,对于每个节点,先将
cur->next
指向pre
,实现反转。 - 更新
pre
和cur
指针,pre
前移至当前节点,cur
前移至下一个待处理的节点。 - 重复步骤2和3,直到
cur
为NULL,即遍历完整个链表。 - 此时
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.设计链表
在设计单向链表时
-
节点初始化:每个节点通常包含至少两个属性:存储数据的
val
和指向下一个节点的指针next
。对于双向链表,还需要一个指向前一个节点的指针prev
。 -
链表的下标:链表的下标通常从0开始,即第一个节点的下标为0,以此类推。
-
头节点和尾节点:在单链表中,头节点通常是一个特殊的节点,它的
val
属性不存储有效数据,它的next
指向链表的第一个有效节点。尾节点的next
指向NULL
,表示链表的结束。
4 初始化代码:在C语言中,节点可以通过 malloc
来动态分配内存,然后初始化 val
和 next
(对于双向链表还有 prev
)。
-
错误处理:在
get
方法中,如果索引无效,应该返回-1
。 -
内存管理:在链表不再使用时,应该释放所有节点的内存,以避免内存泄漏。
结构体定义易错
别名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);
}```