在之前的数据结构知识铺垫2:物理结构与逻辑结构一文中, 我们介绍了物理结构与逻辑结构, 物理结构即存储结构. 本篇文章我们着重探讨一下线性表的逻辑结构与存储结构.
1. 线性表的逻辑结构
图1. 线性表的逻辑结构
线性表是具有相同特性的数据元素的有限序列, 每个元素至多有一个前驱和一个后继.
2. 线性表的存储结构
说到存储结构, 我们就要想到计算机中的内存的特点, 数据元素存储在计算机的内存中, 就只有连续存储和非连续存储两种方式. 线性表的存储结构也可以按照这两种方式分类.
1. 线性表的顺序存储结构
数组是典型的顺序存储结构的线性表.
#include <iostream>
const int MAX_SIZE = 10;
int main()
{
int number[MAX_SIZE]; //为数组开辟的存储空间的大小为MAX_SIZE
int length = 6; //length为数组中的元素个数, length <= MAX_SIZE
for (int i = 0; i < length; i++)
{
number[i] = i;
printf("%d\n", number[i]);
}
}
代码1: 线性表的顺序存储
2. 线性表的链式存储结构
1. 单链表
这里我们引入一个概念: 头结点. 头结点是不含任何数据信息的结点. 因此单链表又可以分为没有头结点的单链表和有头结点的单链表.
我们先回顾一下如何定义一个结构体:
typedef struct LNode {
int data;
struct LNode *next;
}LNode;
代码2: 定义一个结构体
再来看一下这段代码:
#include <iostream>
typedef struct LNode {
int data;
struct LNode* next;
}LNode;
int main()
{
LNode* A;
LNode B;
}
代码3: A是指针类型, B是LNode类型的变量
A和B有什么区别? (以下内容来自Chat GPT)
两种定义方式哪种更好?(以下内容来自Chat GPT)
LNode* A; 与LNode *A; 有什么区别?(以下内容来自Chat GPT)
由于我们后面的学习中会大量地使用对链表的插入删除等操作, 因此我们以后均采用LNode *A;这样的方式. 仅仅是定义结点还不够, 我们还需要给结点分配内存空间. 看下面的代码:
#include <iostream>
typedef struct LNode {
int data;
struct LNode* next;
}LNode;
int main()
{
LNode *A;
A = (LNode*)malloc(sizeof(LNode));
LNode* B;
B = new LNode;
}
代码4: 分配内存空间的两种方式(前者为C, 后者为C++)
A = (LNode*)malloc(sizeof(LNode));什么意思? (以下内容来自Chat GPT)
A = (LNode*)malloc(sizeof(LNode));和A = new LNode;哪种方式更好?(以下内容来自Chat GPT)
因此我们使用new操作符来分配内存空间.
先看没有头结点的单链表.
没有头结点的单链表
图中的Head即为代码的指针p, 并非头结点.
#include <iostream>
typedef struct LNode {
int data;
struct LNode* next;
}LNode;
/// <summary>
/// 遍历输出链表各个结点的值
/// </summary>
/// <param name="firstNode"></param>
void traverseLinkedList(LNode *p) {
while (p != NULL) {
// 对当前节点进行操作,打印节点的数据
printf("%d\n", p->data);
// 移动到下一个节点
p = p->next;
}
}
/// <summary>
/// 判断链表是否非空
/// </summary>
/// <param name="p"></param>
bool linkedListIsNotNull(LNode *p) {
if (p == NULL) {
return false;
}
return true;
}
int main()
{
LNode *A;
A = new LNode;
LNode *B;
B = new LNode;
LNode *C;
C = new LNode;
A->data = 10;
B->data = 20;
C->data = 30;
A->next = B;
B->next = C;
C->next = NULL;
LNode *p = A; //p即为图中的Head
bool isNotNull = linkedListIsNotNull(p);
if (isNotNull) {
traverseLinkedList(p);
}
else {
printf("链表为空!");
}
}
代码5: 没有头结点的单链表
没有头结点的单链表的判空条件: LNode *p = A; 若p == NULL; 则链表为空.
再看有头结点的单链表.
有头结点的单链表
图中的Head即为代码的指针p, 并非头结点, Head指向的结点才是头结点.
#include <iostream>
typedef struct LNode {
int data;
struct LNode* next;
}LNode;
/// <summary>
/// 遍历输出链表各个结点的值
/// </summary>
/// <param name="firstNode"></param>
void traverseLinkedList(LNode *p) {
p = p->next;
while (p != NULL) {
// 对当前节点进行操作,打印节点的数据
printf("%d\n", p->data);
// 移动到下一个节点
p = p->next;
}
}
/// <summary>
/// 判断链表是否非空
/// </summary>
/// <param name="p"></param>
bool linkedListIsNotNull(LNode *p) {
if (p->next == NULL) {
return false;
}
return true;
}
int main()
{
LNode* H;
H = new LNode;
LNode *A;
A = new LNode;
LNode *B;
B = new LNode;
LNode *C;
C = new LNode;
H->data = NULL;
A->data = 10;
B->data = 20;
C->data = 30;
H->next = A;
A->next = B;
B->next = C;
C->next = NULL;
LNode *p = H; //p即为图中的Head
bool isNotNull = linkedListIsNotNull(p);
if (isNotNull) {
traverseLinkedList(p);
}
else {
printf("链表为空!");
}
}
代码6: 有头结点的单链表
有头结点的单链表的判空条件: LNode *p = H; 若p->next == NULL; 则链表为空.
2. 双链表
先看没有头结点的双链表:
没有头结点的双链表
图中的Head即为代码的指针p, 并非头结点.
#include <iostream>
typedef struct LNode {
int data;
struct LNode* next;
struct LNode* prior;
}LNode;
/// <summary>
/// 遍历输出链表各个结点的值
/// </summary>
/// <param name="firstNode"></param>
void traverseLinkedList(LNode *p) {
while (p != NULL) {
// 对当前节点进行操作,打印节点的数据
printf("%d\n", p->data);
// 移动到下一个节点
p = p->next;
}
}
/// <summary>
/// 判断链表是否非空
/// </summary>
/// <param name="p"></param>
bool linkedListIsNotNull(LNode *p) {
if (p == NULL) {
return false;
}
return true;
}
int main()
{
LNode *A;
A = new LNode;
LNode *B;
B = new LNode;
LNode *C;
C = new LNode;
A->data = 10;
B->data = 20;
C->data = 30;
A->next = B;
B->next = C;
C->next = NULL;
A->prior = NULL;
B->prior = A;
C->prior = B;
LNode *p = A; //p即为图中的Head
bool isNotNull = linkedListIsNotNull(p);
if (isNotNull) {
traverseLinkedList(p);
}
else {
printf("链表为空!");
}
}
代码7: 没有头结点的双链表
没有头结点的双链表的判空条件: LNode *p = A; 若p == NULL; 则链表为空.
再看有头结点的双链表:
有头结点的双链表
图中的Head即为代码的指针p, 并非头结点, Head指向的结点才是头结点.
#include <iostream>
typedef struct LNode {
int data;
struct LNode* next;
struct LNode* prior;
}LNode;
/// <summary>
/// 遍历输出链表各个结点的值
/// </summary>
/// <param name="firstNode"></param>
void traverseLinkedList(LNode *p) {
p = p->next;
while (p != NULL) {
// 对当前节点进行操作,打印节点的数据
printf("%d\n", p->data);
// 移动到下一个节点
p = p->next;
}
}
/// <summary>
/// 判断链表是否非空
/// </summary>
/// <param name="p"></param>
bool linkedListIsNotNull(LNode *p) {
if (p->next == NULL) {
return false;
}
return true;
}
int main()
{
LNode* H;
H = new LNode;
LNode *A;
A = new LNode;
LNode *B;
B = new LNode;
LNode *C;
C = new LNode;
H->data = NULL;
A->data = 10;
B->data = 20;
C->data = 30;
H->next = A;
A->next = B;
B->next = C;
C->next = NULL;
H->prior = NULL;
A->prior = H;
B->prior = A;
C->prior = B;
LNode *p = H; //p即为图中的Head
bool isNotNull = linkedListIsNotNull(p);
if (isNotNull) {
traverseLinkedList(p);
}
else {
printf("链表为空!");
}
}
代码8: 有头结点的双链表
有头结点的双链表的判空条件: LNode *p = H; 若p->next == NULL; 则链表为空.
3. 循环链表
1. 单循环链表
先看没有头结点的单循环链表:
没有头结点的单循环链表
图中的Head即为代码的指针p, 并非头结点.
#include <iostream>
typedef struct LNode {
int data;
struct LNode* next;
}LNode;
/// <summary>
/// 遍历输出链表各个结点的值
/// </summary>
/// <param name="firstNode"></param>
void traverseLinkedList(LNode *p) {
int *address = &p->data; //记录首个结点的数据的存放地址, 注意这里要取的是&p->data而不是&p, 因为p只是一个指针, 它的内存地址&p是不会改变的
while (p != NULL) {
// 对当前结点进行操作,打印结点的数据
printf("%d\n", p->data);
// 移动到下一个结点
p = p->next;
if (address == &p->data) { //当p指向的数据的存放地址等于address时, 说明完成了一次遍历, 则跳出循环
break;
}
}
}
/// <summary>
/// 判断链表是否非空
/// </summary>
/// <param name="p"></param>
bool linkedListIsNotNull(LNode *p) {
if (p == NULL) {
return false;
}
return true;
}
int main()
{
LNode *A;
A = new LNode;
LNode *B;
B = new LNode;
LNode *C;
C = new LNode;
A->data = 10;
B->data = 20;
C->data = 30;
A->next = B;
B->next = C;
C->next = A;
LNode *p = A; //p即为图中的Head
bool isNotNull = linkedListIsNotNull(p);
if (isNotNull) {
traverseLinkedList(p);
}
else {
printf("链表为空!");
}
}
代码9: 没有头结点的单循环链表
其中address记录首个结点的数据的存放地址. 注意这里address要取的是&p->data而不是&p, 因为p只是一个指针, 它的内存地址&p是不会改变的.
没有头结点的单循环链表的判空条件: LNode *p = A; 若p == NULL; 则链表为空.
再看有头结点的单循环链表:
有头结点的单循环链表
图中的Head即为代码的指针p, 并非头结点, Head指向的结点才是头结点.
#include <iostream>
typedef struct LNode {
int data;
struct LNode* next;
}LNode;
/// <summary>
/// 遍历输出链表各个结点的值
/// </summary>
/// <param name="firstNode"></param>
void traverseLinkedList(LNode* p) {
p = p->next;
int* address = &p->data; //记录首个结点的数据的存放地址, 注意这里要取的是&p->data而不是&p, 因为p只是一个指针, 它的内存地址&p是不会改变的
while (p != NULL) {
// 对当前结点进行操作,打印结点的数据
printf("%d\n", p->data);
// 移动到下一个结点
p = p->next;
if (address == &p->data) { //当p指向的数据的存放地址等于address时, 说明完成了一次遍历, 则跳出循环
break;
}
}
}
/// <summary>
/// 判断链表是否非空
/// </summary>
/// <param name="p"></param>
bool linkedListIsNotNull(LNode* p) {
if (p->next == p) {
return false;
}
return true;
}
int main()
{
LNode* H;
H = new LNode;
LNode* A;
A = new LNode;
LNode* B;
B = new LNode;
LNode* C;
C = new LNode;
H->data = NULL;
A->data = 10;
B->data = 20;
C->data = 30;
H->next = A;
A->next = B;
B->next = C;
C->next = H;
LNode* p = H; //p即为图中的Head
bool isNotNull = linkedListIsNotNull(p);
if (isNotNull) {
traverseLinkedList(p);
}
else {
printf("链表为空!");
}
}
代码10: 有头结点的单循环链表
有头结点的单循环链表的判空条件: LNode *p = H; 若p->next == p; 则链表为空.
2. 双循环链表
先看没有头结点的双循环链表:
没有头结点的双循环链表
图中的Head即为代码的指针p, 并非头结点.
#include <iostream>
typedef struct LNode {
int data;
struct LNode *next;
struct LNode *prior;
}LNode;
/// <summary>
/// 遍历输出链表各个结点的值
/// </summary>
/// <param name="firstNode"></param>
void traverseLinkedList(LNode* p) {
int* address = &p->data; //记录首个结点的数据的存放地址, 注意这里要取的是&p->data而不是&p, 因为p只是一个指针, 它的内存地址&p是不会改变的
while (p != NULL) {
// 对当前结点进行操作,打印结点的数据
printf("%d\n", p->data);
// 移动到下一个结点
p = p->next;
if (address == &p->data) { //当p指向的数据的存放地址等于address时, 说明完成了一次遍历, 则跳出循环
break;
}
}
}
/// <summary>
/// 判断链表是否非空
/// </summary>
/// <param name="p"></param>
bool linkedListIsNotNull(LNode* p) {
if (p == NULL) {
return false;
}
return true;
}
int main()
{
LNode* A;
A = new LNode;
LNode* B;
B = new LNode;
LNode* C;
C = new LNode;
A->data = 10;
B->data = 20;
C->data = 30;
A->next = B;
B->next = C;
C->next = A;
A->prior = C;
B->prior = A;
C->prior = B;
LNode* p = A; //p即为图中的Head
bool isNotNull = linkedListIsNotNull(p);
if (isNotNull) {
traverseLinkedList(p);
}
else {
printf("链表为空!");
}
}
代码11: 没有头结点的双循环链表
没有头结点的双循环链表的判空条件: LNode *p = A; 若p == NULL; 则链表为空.
再看有头结点的双循环链表:
有头结点的双循环链表.
图中的Head即为代码的指针p, 并非头结点, Head指向的结点才是头结点.
#include <iostream>
typedef struct LNode {
int data;
struct LNode *next;
struct LNode *prior;
}LNode;
/// <summary>
/// 遍历输出链表各个结点的值
/// </summary>
/// <param name="firstNode"></param>
void traverseLinkedList(LNode* p) {
p = p->next;
int* address = &p->data; //记录首个结点的数据的存放地址, 注意这里要取的是&p->data而不是&p, 因为p只是一个指针, 它的内存地址&p是不会改变的
while (p != NULL) {
// 对当前结点进行操作,打印结点的数据
printf("%d\n", p->data);
// 移动到下一个结点
p = p->next;
if (address == &p->data) { //当p指向的数据的存放地址等于address时, 说明完成了一次遍历, 则跳出循环
break;
}
}
}
/// <summary>
/// 判断链表是否非空
/// </summary>
/// <param name="p"></param>
bool linkedListIsNotNull(LNode* p) {
if (p->next == p || p->prior == p) {
return false;
}
return true;
}
int main()
{
LNode* H;
H = new LNode;
LNode* A;
A = new LNode;
LNode* B;
B = new LNode;
LNode* C;
C = new LNode;
H->data = NULL;
A->data = 10;
B->data = 20;
C->data = 30;
H->next = A;
A->next = B;
B->next = C;
C->next = H;
H->prior = C;
A->prior = H;
B->prior = A;
C->prior = B;
LNode* p = H; //p即为图中的Head
bool isNotNull = linkedListIsNotNull(p);
if (isNotNull) {
traverseLinkedList(p);
}
else {
printf("链表为空!");
}
}
代码12: 有头结点的双循环链表
有头结点的双循环链表的判空条件: LNode *p = H;若p->next == p 或 p->prior == p; 则链表为空.
不管是单链表, 双链表还是循环链表, 只要不含头结点, 判空条件均为Head == NULL为真.