目录
- 0 简介
- 1 链表
- 2 单链表
- 2.1 在单链表中插入
- 2.1.1 初次尝试
- 2.1.2 优化插入函数
- 2.1.3 在指定位置插入节点(补充)
- 2.2 其他链表操作
- 2.2.1 单链表的创建
- 2.2.2 单链表的删除
- 2.2.2.1 删除指定位置的节点
- 2.2.2.2 删除整个链表
- 3 双链表
- 3.1 在双链表中插入
- 3.1.1 按顺序插入
- 3.1.2 在指定位置插入节点(补充)
- 3.2 其他链表操作
- 3.2.1 双链表的创建
- 3.2.2 双链表的删除
- 3.2.2.1 删除指定位置的节点
- 3.2.2.2 删除整个链表
- 4 总结
0 简介
纸上得来终觉浅,绝知此事要躬行。前几章学习了结构体、联合体和指针的相关知识。本章就是对这些知识的综合应用。
书中是以链表作为实例的,严格意义上来说,链表属于数据结构与算法的相关内容,关于这方面的知识,想学习的同学推荐《大话数据结构》这本书。书中的内容在通俗易懂的同时,又不乏严谨性。
链表,顾名思义,就是像铁链一样,环环相扣的表。链表是由节点 一个个连起来的,每个节点由两部分组成,分别是数据和指向下一个节点的指针(数据可能不止一个,但是一般的教材和资料的举例中都是一个)。
比较遗憾的是,在本书中,仅仅给了链表的插入操作相关程序,但如果没有了链表的创建和删除(非必需,但最好有)程序,书中的实例是无法运行的。因此,在我们本篇文章中,特意添加了其他所需的相关程序。
书中部分插图有误,请注意甄别。
本章内容提要:
1 链表
如上所述,链表是一种数据结构,由一个个节点构成,通常节点是动态分配的,根据结构的不同,可分为单链表、双链表和环形链表等。本章重点介绍单链表和双链表。
2 单链表
在单链表中,每个节点包含一个指向链表下一个节点的指针。链表最后一个节点的指针字段为NULL
,提示链表后面不再有其他节点。我们来看个单链表的示意图:
root
节点又叫根节点,链表就是基于此进行创建的。链表的创建分成两种,一种是头插法,一种是尾插法。
2.1 在单链表中插入
链式的结构,使得链表在插入和删除新数据的时候比数组效率高很多(平均速度),所以链表的插入操作显得尤为重要。本章重点讲了单链表的插入操作。
2.1.1 初次尝试
标题取的不是很准确,更加准确地说,应该是在单链表中插入节点。我们将书中第一个版本的程序完善后,得到了如下的内容:
先定义节点,声明相关函数,所以定义.h
文件如下:
#pragma once
typedef struct NODE {
struct NODE *link;
int value;
}Node;
int sll_insert(Node *current, int new_value);
然后定义插入函数
,添加如下的.c文件
#include <stdlib.h>
#include <stdio.h>
#include "sll_node.h"
#define FALSE 0
#define TRUE 1
int sll_insert(Node *current, int new_value)
{
Node *previous = (Node *)malloc(sizeof(Node));
Node *new;
while (current->value < new_value)
{
previous = current;
current = current->link;
}
new = (Node *)malloc(sizeof(Node));
if (new == NULL)
return FALSE;
new->value = new_value;
new->link = current;
previous->link = new;
return TRUE;
}
最后写主函数,添加如下的.c文件:
#include <stdlib.h>
#include <stdio.h>
#include "sll_node.h"
//单链表的整表创建
void CreateList(Node *L, int n, int *point)
{
Node *p1, *r;
int i;
r = L;
for (i = 0; i < n; i++)
{
p1 = (Node *)malloc(sizeof(Node));
p1->value = point[i];
r->link = p1;
r = p1;
}
r->link = NULL;
}
//单链表的整表删除
void ClearList(Node *L)
{
Node *p, *q;
p = L->link;
while (p != NULL)
{
q = p->link;
free(p);
p = q;
}
L->link = NULL;
printf("clear all\n");
}
int main()
{
int ele[10];
//链表数值设置
for (int i = 0; i < 10; i++)
{
ele[i] = i * 5;
}
//定义根指针
Node *p = (Node *)malloc(sizeof(Node));
//定义第一个节点的指针
Node *new_head = NULL;
//创建链表
printf("原始链表:\n");
CreateList(p, 10, ele);
//读取并打印链表
new_head = p->link;
while (new_head->link != NULL)
{
printf("%d->\t", new_head->value);
new_head = new_head->link;
}
printf("%d", new_head->value);
printf("\n");
//插入值为8的节点
sll_insert(p, 8);
//再次读取并打印链表
printf("插入节点后的链表:\n");
new_head = p->link;
while (new_head->link != NULL)
{
printf("%d->\t", new_head->value);
new_head = new_head->link;
}
printf("%d", new_head->value);
printf("\n");
//删除整个链表
ClearList(p);
system("pause");
return 0;
}
打印输出如下:
可以看到,已经将节点插入到了所需的位置(位置由节点的值确定),看看节点的具体插入过程(为简便起见,只画出前三个节点)。
我们需要先找到正确的插入位置:
while (current->value < new_value)
{
previous = current;
current = current->link;
}
这部分代码很好理解,如果当前值比新值小,则继续前往下一个节点。如图所示:
直到当前值大于或等于新值:
然后我们再想办法插入新值:
new->value = new_value;
new->link = current;
previous->link = new;
共分成3步:
- 将要插入的值赋给新创建的节点。
- 将新节点的指针指向当前节点。
- 前一个节点的指针指向新节点。
具体过程如下图所示:
至此,链表的新节点插入完成。
2.1.2 优化插入函数
书中这里的优化指的是,若是将一个比第一个节点更小的值传入链表,则会导致程序无法运行,因为传入的是第一个节点。但我们并不存在这个问题,因为我们传入的是根节点。
仅仅有一个小小的地方需要优化,就是在寻找插入位置之前,更改保证当前指针不是空指针,否则不会存在value
。 修改后的程序如下所示(仅展示修改部分):
while (current != NULL && current->value < new_value)
{
previous = current;
current = current->link;
}
2.1.3 在指定位置插入节点(补充)
此外,还有一种插入方法,就是在指定的位置插入节点。实现起来也并不困难。
// 在指定位置插入节点
void sll_insertNode(Node** head, int data, int position)
{
if (*head == NULL || position <= 1) {
// 如果链表为空或插入位置为头部,则直接在头部插入节点
Node* newNode = sll_createLinkedList(data);
newNode->link = *head;
*head = newNode;
}
else {
// 否则在指定位置插入节点
Node* temp = *head;
int currentPosition = 1;
while (currentPosition < position - 1 && temp->link != NULL) {
temp = temp->link;
currentPosition++;
}
if (currentPosition < position - 1) {
printf("插入位置超出链表长度\n");
return;
}
Node* newNode = sll_createLinkedList(data);
newNode->link = temp->link;
temp->link = newNode;
}
}
2.2 其他链表操作
当然,对于链表本身而言,不仅仅只有插入操作,还包含了整个链表的创建和删除。已经在上述代码中有所体现。
2.2.1 单链表的创建
单链表的创建有两种方法,分别是头插法和尾插法。上述的例子中采用了尾插法,因为这样更符合我们的常规思维。
//单链表的整表创建
void CreateList(Node *L, int n, int *point)
{
Node *p1, *r;
int i;
r = L;
for (i = 0; i < n; i++)
{
p1 = (Node *)malloc(sizeof(Node));
p1->value = point[i];
r->link = p1;
r = p1;
}
r->link = NULL;
}
链表的创建过程如下图所示(仅仅给出了前两个节点的创建过程,后面操作类似):
2.2.2 单链表的删除
删除分成两部分,一部分是删除指定位置的节点,另一部分是删除整个链表,前者程序稍微难一些。
2.2.2.1 删除指定位置的节点
同在指定位置插入一样。删除前要先找到位置(当然是前一个节点的位置)。然后再将前一个节点的link指针绕过要删除的节点,直接跳到下一个节点,再删除当前节点,操作完成。
// 删除指定位置的节点
void sll_deleteNode(Node** head, int position)
{
if (*head == NULL) {
return;
}
Node* temp = *head;
int currentPosition = 1;
//找到该节点的前一个节点
while (currentPosition < position - 1 && temp->link != NULL) {
temp = temp->link;
currentPosition++;
}
if (currentPosition < position - 1 || temp->link == NULL) {
printf("删除位置超出链表长度\n");
return;
}
//如果删除头节点,则该节点后移,否则无法访问该链表
if (temp == *head) {
*head = temp->link;
}
//获取当前节点指针,否则到时候无法获取
Node* next_free = temp->link;
//调整指针位置
temp->link = temp->link->link;
free(next_free);
}
2.2.2.2 删除整个链表
删除整个链表就很简单了,逐个访问下一个节点,并删除当前节点即可。
//单链表的整表删除
void sll_deleteLinkedList(Node** head)
{
Node* current = *head;
Node* next;
while (current != NULL) {
next = current->link;
free(current);
current = next;
}
*head = NULL;
}
3 双链表
单链表的替代方案就是双链表。在一个双链表中,每个节点都包含两个指针——指向前一个节点的指针和指向后一个节点的指针。这可以使我们以任何方向遍历双链表,甚至可以忽前忽后地在双链表中访问。下图展示了一个双链表。
3.1 在双链表中插入
3.1.1 按顺序插入
与单链表相比,双链表肯定要难一些,因为需要修改更多的指针。但其基本思想还是不变的,那就是先找需要插入的位置,然后将新节点插入。
先定义节点:
typedef struct NODE {
struct NODE *fwd;
struct NODE *bwd;
int value;
}Node;
再定义插入函数,较之于单链表,双链表要复杂一些:
int dll_insert(Node *rootp, int value)
{
Node *this;
Node *next;
Node *newnode;
//查看value是否已经存在于链表中,如果是就返回
//否则,为新值创建一个节点
for (this = rootp; (next = this->fwd) != NULL; this = next)
{
if (next->value == value)
return 0;
if (next->value > value)
break;
}
newnode = (Node *)malloc(sizeof(Node));
if (newnode == NULL)
return FALSE;
newnode->value = value;
//把新值添加到链表中
if (next != NULL)
{
//情况1或2:并非位于链表尾部
if (this != rootp) /*情况1:并非位于链表的起始位置*/
{
newnode->fwd = next;
this->fwd = newnode;
newnode->bwd = this;
next->bwd = newnode;
}
else /*情况2:位于链表的起始位置*/
{
newnode->fwd = next;
rootp->fwd = newnode;
newnode->bwd = NULL;
next->bwd = newnode;
}
}
else
{
//情况3或4:位于链表的尾部
if (this != rootp)
{
newnode->fwd = NULL;
this->fwd = newnode;
newnode->bwd = this;
rootp->bwd = newnode;
}
else
{
newnode->fwd = NULL;
rootp->fwd = newnode;
newnode->bwd = NULL;
rootp->bwd = newnode;
}
}
return TRUE;
}
最后是主函数的相关程序:
#include <stdlib.h>
#include <stdio.h>
#include "dll_node.h"
int main()
{
Node* head = NULL;
// 创建链表(只有头节点,值为0)
head = dll_createLinkedList(0);
//插入节点
dll_insert(head, 5);
dll_insert(head, 10);
dll_insert(head, 15);
dll_insert(head, 20);
// 删除指定位置的节点
dll_deleteNode(&head, 3);
// 打印链表
dll_printLinkedList(head);
// 删除整个链表
dll_deleteLinkedList(&head);
system("pause");
return 0;
}
参考单链表,多链表的程序便不难看懂。
另外还有链表的打印函数:
void dll_printLinkedList(Node* head) {
Node* temp = head;
while (temp != NULL) {
printf("%d ", temp->value);
if (temp->fwd != NULL)
printf("->");
temp = temp->fwd;
}
}
打印输出:
可以看到,在创建完链表之后,再插入需要的值,最后再将第三个值为10
的节删除,就变成了上面的样子。
3.1.2 在指定位置插入节点(补充)
当然,如果想在指定位置插入节点也是可以的:
void dll_insertNode(Node** head, int data, int position) {
if (*head == NULL || position <= 1) {
// 如果链表为空或插入位置为头部,则直接在头部插入节点
Node* newNode = createLinkedList(data);
newNode->fwd = *head;
if (*head != NULL) {
(*head)->bwd = newNode;
}
*head = newNode;
}
else {
// 否则在指定位置插入节点
Node* temp = *head;
int currentPosition = 1;
while (currentPosition < position - 1 && temp->fwd != NULL) {
temp = temp->fwd;
currentPosition++;
}
if (currentPosition < position - 1) {
printf("插入位置超出链表长度\n");
return;
}
Node* newNode = createLinkedList(data);
newNode->bwd = temp;
newNode->fwd = temp->fwd;
if (temp->fwd != NULL) {
temp->fwd->bwd = newNode;
}
temp->fwd = newNode;
}
}
3.2 其他链表操作
3.2.1 双链表的创建
与单链表类似,双链表也有相同的操作,比如整个链表的创建和删除。
链表的创建程序:
Node* dll_createLinkedList(int data)
{
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败\n");
exit(1);
}
newNode->value = data;
newNode->bwd = NULL;
newNode->fwd = NULL;
return newNode;
}
程序比较好理解,分配一个节点的内存,并得到指向该节点的指针,赋初值,前后均无节点连接。
3.2.2 双链表的删除
3.2.2.1 删除指定位置的节点
对于双链表来说,由于一个节点同时涉及到两个指针,因此删除指定位置的节点相对来说比较难,具体如下。
// 删除指定位置的节点
void dll_deleteNode(Node** head, int position)
{
if (*head == NULL) {
return;
}
Node* temp = *head;
int currentPosition = 1;
while (currentPosition < position && temp != NULL) {
temp = temp->fwd;
currentPosition++;
}
if (currentPosition < position || temp == NULL) {
printf("删除位置超出链表长度\n");
return;
}
if (temp == *head) {
*head = temp->fwd;
}
if (temp->bwd != NULL) {
temp->bwd->fwd = temp->fwd;
}
if (temp->fwd != NULL) {
temp->fwd->bwd = temp->bwd;
}
free(temp);
}
其中最重要的只有4行:
if (temp->bwd != NULL) {
temp->bwd->fwd = temp->fwd;
}
if (temp->fwd != NULL) {
temp->fwd->bwd = temp->bwd;
}
直接让需要删除的节点的前后节点可相互访问,然后再删除当前节点,删除结束。
3.2.2.2 删除整个链表
双链表的删除程序:
void deleteLinkedList(Node** head)
{
Node* current = *head;
Node* next;
while (current != NULL) {
next = current->fwd;
free(current);
current = next;
}
*head = NULL;
}
删除函数也比较好理解,先找到下一个节点,再将当前节点内存释放。这个顺序是不能反的,否则释放完之后,无法获取下一个节点的指针,将导致删除失败。
在此程序中并不需要关注bwd
指针,当我们释放当前节点内存后,其bwd
指针自然也就没有了意义,存储该指针的内存被释放,可以存储其他信息。
4 总结
单链表是一种使用指针来存储值的数据结构。链表中的每个节点包含一个字段,用于指向链表的下一个节点。
双链表中的每个节点包含两个link
字段:其中一个指向链表的下一个节点,另一个指向链表的前一个节点。
除此之外,常见的还有循环链表等数据结构,不过这些都不是很常见。掌握了链表的创建,删除,插入,查找方法(删除不难),就基本上掌握了本章的全部内容。
书中给的链表插入操作是按照顺序插入,而在很多实际案例中,都是按照预先规定的位置插入节点的。这里会有一些差别。而在本文中,两种方法都分别进行了阐述。
---------------------------------------end---------------------------------------