引言
在计算机编程的世界里,数据结构是构建高效算法和程序的核心要素。链表、栈和队列作为基础且重要的数据结构,广泛应用于各种软件开发场景中。本文将结合具体代码,详细解读双向链表的插入与删除、顺序栈和循环队列的基本操作、链表合并以及删除链表倒数第 N 个节点的实现逻辑和代码细节。
1.双向链表插入与删除操作的代码实现
1.1.双向链表节点结构定义
// 定义双向链表节点结构
// 双向链表的每个节点包含三部分:数据域、指向前一个节点的指针和指向后一个节点的指针
typedef struct DNode {
int data; // 存储节点的数据,这里假设数据类型为 int
struct DNode *prev; // 指向前一个节点的指针,用于实现双向遍历
struct DNode *next; // 指向后一个节点的指针,用于正常的链表遍历
} DNode;
双向链表的每个节点包含数据域以及指向前一个节点和后一个节点的指针,这使得我们可以在链表中进行双向移动,为插入和删除操作提供了更多的灵活性。
1.2. 插入操作代码及逻辑
// 在指定位置插入新节点
// 参数:head 为双向链表的头指针,position 为要插入的位置,data 为要插入的数据
// 返回值:插入节点后链表的头指针
DNode* insertAtPosition(DNode *head, int position, int data) {
// 为新节点分配内存空间
DNode *newNode = (DNode*)malloc(sizeof(DNode));
// 检查内存分配是否成功
if (newNode == NULL) {
// 若内存分配失败,输出错误信息并返回原链表头指针
printf("内存分配失败\n");
return head;
}
// 初始化新节点的数据和指针
newNode->data = data;
newNode->prev = NULL;
newNode->next = NULL;
// 处理链表为空且插入位置为 0 的情况
if (head == NULL && position == 0) {
// 直接将新节点作为链表的头节点
return newNode;
}
// 处理在链表头部插入节点的情况
if (position == 0) {
// 新节点的 next 指针指向原头节点
newNode->next = head;
// 若原头节点不为空,更新其 prev 指针指向新节点
if (head != NULL) {
head->prev = newNode;
}
// 新节点成为新的头节点
return newNode;
}
DNode *current = head;
int count = 0;
// 找到要插入位置的前一个节点
while (current != NULL && count < position - 1) {
current = current->next;
count++;
}
// 如果插入位置超出链表长度,释放新节点内存并返回原链表
if (current == NULL) {
printf("插入位置超出链表长度\n");
free(newNode);
return head;
}
// 插入新节点
// 新节点的 next 指针指向当前节点的下一个节点
newNode->next = current->next;
// 若当前节点的下一个节点不为空,更新其 prev 指针指向新节点
if (current->next != NULL) {
current->next->prev = newNode;
}
// 当前节点的 next 指针指向新节点
current->next = newNode;
// 新节点的 prev 指针指向当前节点
newNode->prev = current;
return head;
}
插入操作的逻辑在于根据插入位置的不同进行不同的处理。当链表为空且插入位置为 0 时,新节点直接成为头节点。若在头部插入,更新新节点和原头节点的指针。对于其他位置,需要先找到插入位置的前一个节点,然后依次调整新节点与前后节点的指针,确保链表的双向连接性不被破坏。
1.3.删除操作代码及逻辑
// 删除指定位置的节点
// 参数:head 为双向链表的头指针,position 为要删除的位置
// 返回值:删除节点后链表的头指针
DNode* deleteAtPosition(DNode *head, int position) {
// 如果链表为空,直接返回 NULL
if (head == NULL) {
return NULL;
}
DNode *current = head;
// 处理删除头节点的情况
if (position == 0) {
// 更新头节点为原头节点的下一个节点
head = head->next;
// 若新头节点不为空,将其 prev 指针置为 NULL
if (head != NULL) {
head->prev = NULL;
}
// 释放原头节点的内存
free(current);
return head;
}
int count = 0;
// 找到要删除的节点
while (current != NULL && count < position) {
current = current->next;
count++;
}
// 如果删除位置超出链表长度,返回原链表
if (current == NULL) {
printf("删除位置超出链表长度\n");
return head;
}
// 调整指针以删除节点
// 若当前节点的前一个节点不为空,更新其 next 指针跳过当前节点
if (current->prev != NULL) {
current->prev->next = current->next;
}
// 若当前节点的下一个节点不为空,更新其 prev 指针跳过当前节点
if (current->next != NULL) {
current->next->prev = current->prev;
}
// 释放当前节点的内存
free(current);
return head;
}
删除操作首先判断链表是否为空。若要删除头节点,更新头节点为原头节点的下一个节点,并将新头节点的 prev
指针置为 NULL
。对于其他位置的节点,找到要删除的节点后,更新其前后节点的指针,最后释放该节点的内存,保证链表的连续性。
2.顺序栈基本操作的代码解析
2.1.顺序栈结构定义
顺序栈基于数组实现,通过 top
指针来跟踪栈顶元素的位置,遵循后进先出(LIFO)的原则。
// 定义顺序栈结构
// 顺序栈使用数组来存储元素,通过 top 指针来指示栈顶位置
typedef struct {
int data[MAX_SIZE]; // 用于存储栈中元素的数组,大小为 MAX_SIZE
int top; // 栈顶指针,初始为 -1 表示栈为空
} Stack;
2.2.初始化与判空操作
// 初始化栈
// 参数:s 为指向栈结构体的指针
void initStack(Stack *s) {
s->top = -1; // 将栈顶指针初始化为 -1,表示栈为空
}
// 判断栈是否为空
// 参数:s 为指向栈结构体的指针
// 返回值:若栈为空返回 1,否则返回 0
int isEmpty(Stack *s) {
return s->top == -1; // 若栈顶指针为 -1,则栈为空
}
初始化栈时,将 top
指针置为 -1,表示栈为空。判断栈是否为空只需检查 top
指针的值。
2.3.入栈与出栈操作
// 入栈操作
// 参数:s 为指向栈结构体的指针,value 为要入栈的元素
void push(Stack *s, int value) {
// 检查栈是否已满
if (s->top == MAX_SIZE - 1) {
// 若栈已满,输出错误信息
printf("栈已满,无法入栈\n");
return;
}
// 先将栈顶指针加 1,再将元素放入栈顶位置
s->data[++(s->top)] = value;
}
// 出栈操作
// 参数:s 为指向栈结构体的指针
// 返回值:若栈不为空,返回栈顶元素;若栈为空,返回 -1 表示操作失败
int pop(Stack *s) {
// 检查栈是否为空
if (isEmpty(s)) {
// 若栈为空,输出错误信息并返回 -1
printf("栈为空,无法出栈\n");
return -1;
}
// 先返回栈顶元素,再将栈顶指针减 1
return s->data[(s->top)--];
}
入栈操作前,先检查栈是否已满。若未满,则将 top
指针加 1,并将元素放入新的栈顶位置。出栈操作时,先检查栈是否为空,若不为空,则返回栈顶元素,并将 top
指针减 1。
2.4.获取栈顶元素操作
// 获取栈顶元素
// 参数:s 为指向栈结构体的指针
// 返回值:若栈不为空,返回栈顶元素;若栈为空,返回 -1 表示操作失败
int peek(Stack *s) {
// 检查栈是否为空
if (isEmpty(s)) {
// 若栈为空,输出错误信息并返回 -1
printf("栈为空,无栈顶元素\n");
return -1;
}
// 返回栈顶元素
return s->data[s->top];
}
获取栈顶元素时,同样要先检查栈是否为空,若不为空则返回 top
指针所指向的元素。
3.循环队列基本操作的代码探究
3.1.循环队列结构定义
循环队列利用数组实现,通过 front
和 rear
指针来管理队列的头部和尾部,借助取模运算实现队列的循环使用。
// 定义循环队列结构
// 循环队列使用数组存储元素,通过 front 和 rear 指针实现循环操作
typedef struct {
int data[MAX_SIZE]; // 用于存储队列元素的数组,大小为 MAX_SIZE
int front; // 队头指针,指向队列头部元素的位置
int rear; // 队尾指针,指向队列尾部元素的下一个位置
} CircularQueue;
3.2.初始化与判空、判满操作
// 初始化循环队列
// 参数:q 为指向循环队列结构体的指针
void initQueue(CircularQueue *q) {
q->front = 0; // 队头指针初始化为 0
q->rear = 0; // 队尾指针初始化为 0
}
// 判断队列是否为空
// 参数:q 为指向循环队列结构体的指针
// 返回值:若队列为空返回 1,否则返回 0
int isQueueEmpty(CircularQueue *q) {
return q->front == q->rear; // 若队头指针等于队尾指针,则队列为空
}
// 判断队列是否已满
// 参数:q 为指向循环队列结构体的指针
// 返回值:若队列已满返回 1,否则返回 0
int isQueueFull(CircularQueue *q) {
return (q->rear + 1) % MAX_SIZE == q->front;
// 当队尾指针的下一个位置(通过取模运算实现循环)等于队头指针时,队列已满
}
初始化循环队列时,将 front
和 rear
指针都置为 0。判断队列是否为空,只需检查 front
是否等于 rear
;判断队列是否已满,则通过 (rear + 1) % MAX_SIZE == front
来实现。
3.3.入队与出队操作
// 入队操作
// 参数:q 为指向循环队列结构体的指针,value 为要入队的元素
void enqueue(CircularQueue *q, int value) {
// 检查队列是否已满
if (isQueueFull(q)) {
// 若队列已满,输出错误信息
printf("队列已满,无法入队\n");
return;
}
// 将元素放入队尾位置
q->data[q->rear] = value;
// 队尾指针循环移动到下一个位置
q->rear = (q->rear + 1) % MAX_SIZE;
}
// 出队操作
// 参数:q 为指向循环队列结构体的指针
// 返回值:若队列不为空,返回队头元素;若队列为空,返回 -1 表示操作失败
int dequeue(CircularQueue *q) {
// 检查队列是否为空
if (isQueueEmpty(q)) {
// 若队列为空,输出错误信息并返回 -1
printf("队列为空,无法出队\n");
return -1;
}
// 取出队头元素
int value = q->data[q->front];
// 队头指针循环移动到下一个位置
q->front = (q->front + 1) % MAX_SIZE;
return value;
}
入队操作前,先检查队列是否已满。若未满,则将元素放入 rear
指针所指向的位置,并将 rear
指针通过取模运算循环移动到下一个位置。出队操作时,先检查队列是否为空,若不为空,则取出 front
指针所指向的元素,并将 front
指针通过取模运算循环移动到下一个位置。
3.4.获取队列头元素操作
// 获取队列头元素
// 参数:q 为指向循环队列结构体的指针
// 返回值:若队列不为空,返回队头元素;若队列为空,返回 -1 表示操作失败
int getFront(CircularQueue *q) {
// 检查队列是否为空
if (isQueueEmpty(q)) {
// 若队列为空,输出错误信息并返回 -1
printf("队列为空,无队头元素\n");
return -1;
}
// 返回队头元素
return q->data[q->front];
}
获取队列头元素时,先检查队列是否为空,若不为空则返回 front
指针所指向的元素。
4.链表合并与删除链表倒数第 N 个节点
4.1.链表合并代码及逻辑
// 定义单向链表节点结构
// 单向链表的每个节点包含数据域和指向下一个节点的指针
typedef struct Node {
int data; // 存储节点的数据,这里假设为 int 类型
struct Node *next; // 指向下一个节点的指针
} Node;
// 合并两个单链表
// 参数:p 和 p1 分别为两个单向链表的头指针
// 返回值:合并后链表的头指针
Node* mergeLists(Node *p, Node *p1) {
// 如果链表 p 为空,直接返回链表 p1
if (p == NULL) {
return p1;
}
// 如果链表 p1 为空,直接返回链表 p
if (p1 == NULL) {
return p;
}
Node *current = p;
// 找到链表 p 的尾节点
while (current->next != NULL) {
current = current->next;
}
// 将链表 p1 连接到链表 p 的尾部
current->next = p1;
return p;
}
链表合并操作的逻辑较为简单。若其中一个链表为空,则直接返回另一个链表。否则,找到第一个链表的尾节点,将其 next
指针指向第二个链表的头节点,从而实现两个链表的合并。
4.2.删除链表倒数第 N 个节点代码及逻辑
// 删除链表的倒数第 N 个节点
// 参数:head 为单向链表的头指针,n 为要删除的倒数第几个节点
// 返回值:删除节点后链表的头指针
Node* removeNthFromEnd(Node *head, int n) {
// 创建一个虚拟头节点,方便处理删除头节点的情况
Node *dummy = (Node*)malloc(sizeof(Node));
dummy->data = 0;
dummy->next = head;
Node *first = dummy;
Node *second = dummy;
// 让 first 指针先移动 n + 1 步
for (int i = 0; i <= n; i++) {
first = first->next;
}
// 同时移动 first 和 second 指针,直到 first 指针到达链表末尾
while (first != NULL) {
first = first->next;
second = second->next;
}
// 此时 second 指针指向要删除节点的前一个节点
Node *temp = second->next;
// 调整指针跳过要删除的节点
second->next = second->next->next;
// 释放要删除节点的内存
free(temp);
// 获取新的头节点
Node *result = dummy->next;
// 释放虚拟头节点的内存
free(dummy);
return result;
}
删除链表倒数第 N 个节点使用双指针法。首先创建一个虚拟头节点,让第一个指针先移动 N + 1
步,然后两个指针同时移动,当第一个指针到达链表末尾时,第二个指针指向要删除节点的前一个节点,调整指针并释放要删除节点的内存。
4.3.其他方法实现
使用两个指针 fast`` 和 slow``,让 fast 先移动 N 步,然后同时移动 fast 和 slow,直到 fast 指向链表末尾。此时 slow 指向倒数第 N+1 个节点(待删除节点的前驱)123。
特殊处理:若链表长度等于 N,直接删除头节点。
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
// 添加哨兵节点,简化头节点删除逻辑[2]()[3]()
struct ListNode dummy = {0, head};
struct ListNode *fast = &dummy, *slow = &dummy;
// fast先移动n+1步(因为包含哨兵节点)
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
// 同时移动fast和slow,直到fast为NULL
while (fast) {
fast = fast->next;
slow = slow->next;
}
// 删除目标节点
slow->next = slow->next->next;
return dummy.next;
}
创建一个虚拟头节点 dummy,通过比较两个链表的当前节点值,按升序逐个连接节点,最后返回 dummy.next 。
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
struct ListNode dummy = {0, NULL};
struct ListNode *tail = &dummy;
while (l1 && l2) {
if (l1->val < l2->val) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
// 连接剩余节点
tail->next = l1 ? l1 : l2;
return dummy.next;
}
合并无序链表(简单拼接)若需直接拼接两个链表:
struct ListNode* merge(struct ListNode* list1, struct ListNode* list2) {
if (!list1) return list2;
struct ListNode* p = list1;
while (p->next) {
p = p->next;
}
p->next = list2;
return list1;
}