带头结点的单链表
如果说,顺序表是逻辑相邻,物理也相邻的话,那么链表就是逻辑相邻,物理不一定相邻了。对应在计算机中,顺序表,是开创了一块连续的内存空间,用于存放一定的数据。那么链表开辟的内存空间就不一定是连续的、同一块空间了。
顺序表(不定长)示意图:
单链表(带头结点)示意图:
插入图解
对于插入来说,假定:要插入位置的结点为p;插入新的结点为n。那么到底是先p->next = n->next
呢?还是先n->next = p->next
呢?
对于这个问题,我们可以用如下图来理解:
头文件(.h)
接下来我们来看头文件:
#pragma once
// 带头结点的单链表,尾结点的指针域为NULL
// 头结点:开头的标识,类似旗帜,不存放数据且不参与运算
// 数据结点:存放数据
typedef struct Node
{
int data; // 保存数据
struct Node* next; // 下一个结点
}Node,*List;
// 初始化函数
void InitList(List plist);
// 判空
bool IsEmpty(List plist);
// 获取数据长度
int GetLength(List plist);
// 头插
bool Insert_head(List plist, int val);
// 尾插
bool Insert_tail(List plist, int val);
// 在plist中查找关键字key,找到返回目标地址,失败返回NULL
Node* Search(List plist, int key);
// 删除plist中的第一个key
bool DeleteVal(List plist, int key);
// 打印输出所有数据
void Show(List plist);
// 逆置(重中之重)
void Reverse(List plist);
// 清空数据
void Clear(List plist);
// 销毁动态内存
void Destroy(List plist);
代码文件(.cpp)
接下来就是代码的实现了:
#include <stdio.h> // 输入输出
#include <stdlib.h> // 动态内存
#include <assert.h> // 断言
#include "list.h"
// 初始化函数
void InitList(List plist)
{
assert(plist != NULL);
if (plist == NULL) return;
// 将头结点的next赋值为空
plist->next = NULL;
}
// 判空
bool IsEmpty(List plist)
{
assert(plist != NULL);
if (plist == NULL) return false;
// 如果头结点的next结点为NULL,说明没有一个数据结点,即为空
return plist->next == NULL;
}
// 获取数据长度
int GetLength(List plist)
{
assert(plist != NULL);
if (plist == NULL) return -1;
int count = 0;
for (Node* p = plist; p->next != NULL; p = p->next)
{
count += 1;
}
return count;
}
// 头插
bool Insert_head(List plist, int val)
{
assert(plist != NULL);
if (plist == NULL) return false;
Node* p = (Node*)malloc(sizeof(Node));
p->data = val;
// 先将原来头结点的next赋值给新结点的next
p->next = plist->next;
// 再将新结点赋值给头结点的next
plist->next = p;
return true;
}
// 尾插
bool Insert_tail(List plist, int val)
{
assert(plist != NULL);
if (plist == NULL) return false;
// 创建新结点
Node* p = (Node*)malloc(sizeof(Node));
// 将值存放到新结点
p->data = val;
// 找尾结点
Node* q;
for (q = plist; q->next != NULL; q = q->next)
{
// 不用操作,跟着循环定位到最后一个结点即可
;
}
// 将新结点p插入在尾结点q的后面
// p->next = NULL; 与下方语句等价
// 因为q本身就是最后一个结点,q->next == NULL
p->next = q->next;
q->next = p;
return true;
}
// 在plist中查找关键字key,找到返回目标地址,失败返回NULL
Node* Search(List plist, int key)
{
assert(plist != NULL);
if (plist == NULL) return NULL;
// 注意:起始条件p初始化成第一个数据结点而不是头结点
for (Node* p = plist->next; p != NULL; p = p->next)
{
if (key == p->data)
{
return p;
}
}
// 遍历了一遍没找到,即失败返回NULL
return NULL;
}
// 查找key的前驱结点,成功返回key的前驱,失败返回NULL
static Node* SearchPrio(List plist, int key)
{
assert(plist != NULL);
if (plist == NULL) return NULL;
// 注意:此处起始条件p初始化成头结点
for (Node* p = plist; p->next != NULL; p = p->next)
{
if (key == p->next->data)
{
return p;
}
}
return NULL;
}
// 删除plist中的第一个key
bool DeleteVal(List plist, int key)
{
assert(plist != NULL);
if (plist == NULL) return false;
// p定位到要删除的key的前驱结点
Node* p = SearchPrio(plist,key);
if (p == NULL)
{
// 若查找失败,删除也失败
return false;
}
// q指向要删除的结点
Node* q = p->next;
// 将q从链表中删除
p->next = q->next;
free(q);
return true;
}
// 打印输出所有数据
void Show(List plist)
{
assert(plist != NULL);
if (plist == NULL) return;
for (Node* p = plist->next; p != NULL; p = p->next)
{
printf("%d ",p->data);
}
printf("\n");
}
// 逆置(重中之重)
void Reverse(List plist)
{
// 利用头插的思想
assert(plist != NULL);
if (plist == NULL) return;
Node* p = plist->next;
Node* q;
plist->next = NULL;
while (p != NULL)
{
q = p->next;
//将p头插到plist中
p->next = plist->next;
plist->next = p;
p = q;
}
}
// 清空数据
void Clear(List plist)
{
assert(plist != NULL);
if (plist == NULL) return;
plist->next = NULL;
}
// 销毁动态内存
void Destroy(List plist)
{
assert(plist != NULL);
if (plist == NULL) return;
Node* p;
while (plist->next != NULL)//还有第一个数据节点
{
p = plist->next;//p指向第一个数据节点
plist->next = p->next;//将p从链表中剔除
free(p);
}
}
简单测试
我们给出以下的测试代码:
#include <stdio.h>
#include <iostream>
#include "list.h"
int main()
{
Node head;
InitList(&head);
printf("头插:0-5\n");
for (int i = 0; i < 5; i++)
{
Insert_head(&head, i);
}
Show(&head);
Clear(&head);
printf("尾插:0-5\n");
for (int i = 0; i < 5; i++)
{
Insert_tail(&head, i);
}
Show(&head);
printf("获取长度:%d\n", GetLength(&head));
printf("请输入要查找的关键字:");
// C语言的scanf不好用,我们这里用C++的cin
int _key = 0;
std::cin >> _key;
std::cout << Search(&head, _key) << std::endl;
printf("请输入要删除的值:");
int del = 0;
std::cin >> del;
DeleteVal(&head, del);
Show(&head);
std::cout << "逆置链表:" << std::endl;
Reverse(&head);
Show(&head);
Destroy(&head);
return 0;
}
输出结果:
第一次我们给的查找关键字是:5
但是我们可以看到单链表中的数据只有0~4;所以,查找失败,返回NULL;我们把这个返回的值打印出来就是00000000;
同样的,删除的时候,链表中也没有5,所以删除也是失败的,但是因为本来就没有这个数据,也可以某种程度上认为是删除成功。不影响后续的输出。
第二次我们给的查找关键字是:4
对比之前的结果我们可以看到,查找是成功的,返回了关键字4的地址;
同样的,下面删除的时候,因为链表中是有4的,所以删除也成功了;
最后的逆置也没有问题。
参考资料
【1】严蔚敏. 数据结构(C语言版). 北京:清华大学出版社,2009:30.