一.线性表
1.线性表的定义
线性表是具有相同的数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。如果用L命名线性表,则其一般表示为
L=(a1,a2,…ai,ai+1,…an)
表达式中,a1是唯一的”第一个“数据元素,又称表头元素;an是唯一的”最后一个“数据元素”,又称表尾元素。除第一个元素外,每个元素有且仅有一个直接前驱。除最后一个元素外,每个元素有且仅有一个直接后继。
2.线性表的特点
1.表中的元素个数有限。
2.表中元素具有逻辑上的顺序性,有先后次序。
3.表中元素都是数据元素,每个元素都是单个元素。
4.表中元素的数据类型都相同,也就是每个元素存储空间大小相同。
5.表中元素具有抽象性,仅考虑元素之间的逻辑关系,而不考虑元素表示什么内容。
二.线性表的顺序表示
1.顺序表的定义和特点
1.定义
线性表的顺序存储又称顺序表。顺序表(Sequential List)是一种线性表的数据结构,它采用一段连续的存储单元依次存储线性表的数据元素。在顺序表中,数据元素之间的逻辑关系是通过元素在存储空间中的相对位置来表示的,即数据元素在顺序表中的位置(或索引)决定了元素之间的相对顺序。
具体来说,顺序表可以通过数组来实现。在数组中,每个元素都有一个索引(或下标),索引用于唯一标识数组中的元素,并且索引之间的差值表示了元素之间的相对位置。顺序表中的第一个元素存储在数组的起始位置(索引为0或1,取决于编程语言的约定),后续的元素依次存储在数组的后续位置。
假设顺序表存储的起始位置为LOC(A),sizeof(SLDataType)是每个数据元素所占用存储空间的大小,则顺序表的存储如图所示
2.特点
随机访问:顺序表支持随机访问,即可以通过索引直接访问表中的任意元素,时间复杂度为O(1)。
存储空间连续:顺序表在物理存储上是连续的,这有利于CPU的缓存机制,提高数据的访问效率。
容量限制:顺序表在创建时需要指定一个固定的大小(容量),当元素数量超过这个容量时,需要进行扩容操作。扩容通常涉及重新分配更大的内存空间,并将旧数据复制到新空间中,这个过程可能会消耗较多的时间和资源。
插入和删除操作:在顺序表中,特别是在中间或前面的位置插入或删除元素时,需要移动大量的元素,以保持顺序表的连续性,这可能会导致效率较低。
2.顺序表的基本操作
顺序表分为静态分配和动态分配
静态分配的顺序表存储结构描述为
typedef int SLDataType; typedef struct seqList{ SLDataType a[maxsize];//maxsize为顺序表的元素个数 int size; //顺序表的当前个数 }seqList;
动态分配的顺序表存储结构描述为
typedef int SLDataType; typedef struct seqList { SLDataType* a;//动态分配数组的指针 int size; //数组的当前个数 int capacity; //数组的最大容量 }seqList;
对数组进行静态分配时,因为数组的大小和空间事先已经固定,所以一旦空间占满,再加入新数据就会产生溢出。而在动态分配时,一旦数据空间占满,就会另外开辟一块更大的存储空间,将原表中的元素全部拷贝到新空间,从而达到扩充数组存储空间的目的,而不需要为线性表一次性地划分所有空间。所以下面将会以动态分配的顺序表为例来讲述顺序表的基本操作
1.初始化
void Initslt(seqList* ps) {
ps->a = (SLDataType*)malloc(sizeof(SLDataType)*INIT_capacity);
if (ps->a == NULL) {
perror("malloc false");
return;
}
ps->capacity = INIT_capacity;
ps->size = 0;
}
2.遍历
void Printslt(seqList* ps) {
for (int i = 0; i < ps->size; i++) {
printf("%d ", ps->a[i]);
}
printf("\n");
}
3.扩容
void checkcapacity(seqList* ps) {
if (ps->capacity == ps->size) {
SLDataType* tmp = (SLDataType*)realloc(ps->a, ps->capacity * 2 * (sizeof(SLDataType)));
if (tmp == NULL) {
perror("realloc false");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
}
4.尾插
void pushrear(seqList* ps,SLDataType x) {
checkcapacity(ps);
ps->a[ps->size++] = x;
}
5.尾删
void poprear(seqList* ps) {
if (ps->size == 0) {
return;
}
ps->size--;
}
6.头插
void pushfront(seqList* ps,SLDataType x) {
assert(ps);
checkcapacity(ps);
int end = ps->size - 1;
while (end >= 0) {
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
7.头删
void popfront(seqList* ps) {
assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size) {
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
8.在pos节点插入数
void SLInsert(seqList* ps, int pos, SLDataType x) {
assert(ps);
checkcapacity(ps);
int end = ps->size - 1;
while (end >= pos) {
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
9.删除pos节点的数
void SLErase(seqList* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size) {
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
10.销毁
void Destroy(seqList* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
11.测试用例
ps:以下测试用例仅供参考,可以尝试带入其他的数值或调用其他的函数来测试
#define INIT_capacity 4
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
void test1() {
seqList s; //创建顺序表s
initslt(&s); //初始化s
pushrear(&s, 1); //用尾插法依次插入1 2 3 4 5
pushrear(&s, 2);
pushrear(&s, 3);
pushrear(&s, 4);
pushrear(&s, 5);
Printslt(&s); //遍历打印s
SLInsert(&s, 3, 20);//在下标为3插入20
SLInsert(&s, 6, 10);//在下标为6插入10
Printslt(&s); //遍历打印s
SLErase(&s, 2); //删除下标为2的数
Printslt(&s); //遍历打印s
Destroy(&s); //销毁s
}
void test2() {
seqList s; //创建顺序表s
Initslt(&s); //初始化s
pushfront(&s, 1); //用头插法依次插入1 2 3 4 5
pushfront(&s, 2);
pushfront(&s, 3);
pushfront(&s, 4);
pushfront(&s, 5);
Printslt(&s); //遍历打印s
popfront(&s); //删除第一个数
Printslt(&s); //遍历打印s
SLInsert(&s, 3, 10);//在下标为3插入10
Printslt(&s); //遍历打印s
SLErase(&s, 2); //删除下标为2的数
Printslt(&s); //遍历打印s
Destroy(&s); //销毁s
}
int main() {
test1();
test2();
return 0;
}
测试结果如下:
三.顺序表的链式表示
1.单链表的定义和特点
1.定义
线性表的链式存储又称单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。单链表是一种常见的数据结构,它由一系列节点(Node)组成,每个节点包含两个部分:数据域(data field)和指针域(pointer field 或 link field)。数据域用于存储节点的数据,而指针域则用于存储指向列表中下一个节点的指针(在最后一个节点处,该指针通常指向NULL或某个特定的“空”节点,以表示链表的结束)。
2.特点
非连续性存储:与数组不同,单链表中的节点在物理内存中不必连续存储,这使得链表在插入和删除节点时更加灵活和高效。
动态分配内存:链表的节点通常是通过动态内存分配(如使用
malloc
或new
)创建的,这允许链表根据需要增长或缩小。需要额外空间:每个节点都需要额外的空间来存储指向下一个节点的指针,这增加了链表的内存开销。
访问效率低:由于节点在内存中不是连续存储的,因此不能通过简单的索引来直接访问链表中的元素,而是需要从头节点开始遍历链表。
2.单链表的基本操作
以不带头节点的单链表为例
单链表中节点类型的描述如下:
typedef int SLNDatatype; typedef struct SListNode { SLNDatatype data;//数据域 struct SListNode* next;//指针域 }SListNode;
1.初始化
void InitList(SListNode** phead) {
//不带头节点,直接为空即可
*phead = NULL;
}
2.遍历打印链表
void SLTPrint(SListNode* phead) {
SListNode* cur = phead;
while (cur) {
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
3.查找
SListNode* SLTFind(SListNode* phead, SLNDatatype x) {
SListNode* cur = phead;
while (cur) {
if (cur->data == x) {
return cur;
}
cur = cur->next;
}
return NULL;
}
4.创建节点
SListNode* createListNode(SLNDatatype x) {
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
if (newNode == NULL) {
perror("malloc false");
return NULL;
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
5.尾插
void pushrear(SListNode** pphead, SLNDatatype x) {
assert(pphead);
SListNode* newNode = createListNode(x);
if (*pphead == NULL) {
*pphead = newNode;
}
else {
SListNode* p = *pphead;
while (p->next) {
p = p->next;
}
p->next = newNode;
}
}
6.尾删
void poprear(SListNode** pphead) {
assert(pphead);
assert(*pphead);
if ((*pphead)->next = NULL) {
free(*pphead);
*pphead = NULL;
}
else {
SListNode* per = NULL;
SListNode* curr = *pphead;
while (curr->next) {
per = curr;
curr = curr->next;
}
per->next = NULL;
free(curr);
curr = NULL;
}
}
7.头插
void pushfront(SListNode** pphead, SLNDatatype x) {
assert(pphead);
SListNode* newNode = createListNode(x);
newNode->next = *pphead;
*pphead = newNode;
}
8.头删
void popfront(SListNode** pphead) {
assert(pphead);
assert(*pphead);
SListNode* newhead = (*pphead)->next;
free(*pphead);
*pphead = newhead;
}
9.在pos节点前面插入
void SLTFInsert(SListNode** pphead, SListNode* pos, SLNDatatype x) {
assert(pphead);
assert(*pphead);
assert(pos);
if (pos == (*pphead)) {
pushfront(pphead, x);
}
else {
SListNode* newNode = createListNode(x);
SListNode* per = *pphead;
while (per->next != pos) {
per = per->next;
}
newNode->next = pos;
per->next = newNode;
}
}
10.在pos节点后面插入
void SLTRInsert(SListNode** pphead, SListNode* pos, SLNDatatype x) {
assert(pphead);
assert(pos);
SListNode* newNode = createListNode(x);
newNode->next = pos->next;
pos->next = newNode;
}
11.删除pos节点后面的节点
void SLTREarse(SListNode** pphead, SListNode* pos) {
assert(pphead);
assert(pos);
assert(pos->next);
SListNode* per = pos->next;
pos->next = per->next;
free(per);
per = NULL;
}
12.删除pos节点
void SLTEarse(SListNode** pphead, SListNode* pos) {
assert(pphead);
assert(pos);
if (*pphead == pos) {
popfront(pphead);
}
else {
SListNode* per = *pphead;
while (per->next != pos) {
per = per->next;
}
per->next = pos->next;
free(pos);
}
}
13.销毁
void SLTdestroy(SListNode** pphead) {
assert(pphead);
SListNode* cur = *pphead;
SListNode* tmp;
while (cur) {
tmp = cur->next;
free(cur);
cur = tmp;
}
*pphead = NULL;
}
14.测试用例
ps:以下测试用例仅供参考,可以尝试带入其他的数值或调用其他的函数来测试
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
void test1() {
SListNode* plist; //创建链表plist
InitList(&plist); //初始化链表
pushrear(&plist, 1); //用尾插法依次插入1 2 3 4 5
pushrear(&plist, 2);
pushrear(&plist, 3);
pushrear(&plist, 4);
pushrear(&plist, 5);
SLTPrint(plist); //遍历打印链表
SListNode *ret=SLTFind(plist, 2);//查找值为2的节点为ret
ret->data *= 3; //将ret节点的值乘以3
SLTFInsert(&plist, ret, 10); //在ret节点前插入值为10的节点
SLTPrint(plist); //遍历打印插入后的链表
SLTREarse(&plist, ret); //删除ret节点后面的节点
SLTPrint(plist); //遍历打印删除后的链表
SLTEarse(&plist, ret); //删除ret节点
SLTPrint(plist); //遍历打印删除后的链表
SLTDestroy(&plist); //销毁链表
}
void test2() {
SListNode* plist; //创建链表plist
InitList(&plist); //初始化链表
pushfront(&plist, 1); //用头插法依次插入1 2 3 4 5
pushfront(&plist, 2);
pushfront(&plist, 3);
pushfront(&plist, 4);
pushfront(&plist, 5);
SLTPrint(plist); //遍历打印链表
SListNode* p = SLTFind(plist, 4);//查找值为4的节点为ret
p->data *= 3; //将ret节点的值乘以3
SLTRInsert(&plist, p, 30); //在ret节点前插入值为30的节点
SLTPrint(plist); //遍历打印插入后的链表
SLTREarse(&plist, p); //删除ret节点后面的节点
SLTPrint(plist); //遍历打印删除后的链表
SLTEarse(&plist, p); //删除ret节点
SLTPrint(plist); //遍历打印删除后的链表
SLTDestroy(&plist); //销毁链表
}
int main() {
test1();
test2();
return 0;
}
test1测试结果如下:
test2测试结果如下:
3.双链表的定义和特点
1.定义
双链表(Double-Linked List)是链表的一种,相比于单链表,它的每个节点不仅包含数据域,还具备两个指针域,分别指向前一个节点和后一个节点。这样的结构赋予了双链表更高的操作灵活性和更多的应用场景。
typedef struct DNode { DataType data; // 数据域,用于存储数据 struct DNode* prev; // 前驱指针域,指向前一个节点 struct DNode* next; // 后继指针域,指向后一个节点 }DNode;
其中,
DataType
是节点存储的数据类型,DNode* prev
是指向前一个节点的指针,而DNode* next
是指向后一个节点的指针。在双链表的第一个节点(通常称为头节点)中,prev
指针可能不指向任何节点(即为NULL),或者指向链表的最后一个节点(如果实现的是双向循环链表)。类似地,在最后一个节点中,next
指针将指向NULL或头节点(在双向循环链表中)。
2.特点
双向访问能力:从双链表中的任意一个节点开始,都可以很方便地访问它的前驱节点和后继节点,这使得双向链表在进行插入、删除和遍历等操作时更加灵活。
动态分配内存:与单链表一样,双链表的节点也是通过动态内存分配创建的,允许链表根据需要增长或缩小。
需要额外空间:每个节点需要额外的空间来存储两个指针,因此双链表的内存开销比单链表更大。
应用场景广泛:双链表在实际应用中扮演着重要的角色,特别是在需要频繁进行节点插入和删除操作的场景中,如实现撤销操作、LRU缓存淘汰算法等。
4.循环链表的定义和特点
1.定义
循环链表(Circular Linked List)是链表的一种特殊形式,它的主要特点是链表中最后一个节点的指针域不是指向NULL,而是指向链表的第一个节点(或头节点),从而形成一个环。这种结构使得链表在逻辑上成为了一个闭环。
循环链表可以是单向的,也可以是双向的:
单向循环链表:在这种链表中,每个节点只有一个指向下一个节点的指针。最后一个节点的指针指向链表的第一个节点,形成一个单向的环路。
双向循环链表:在这种链表中,每个节点除了有一个指向下一个节点的指针外,还有一个指向前一个节点的指针。第一个节点的前驱指针指向最后一个节点,而最后一个节点的后继指针指向第一个节点,从而形成一个双向的环路。
2.特点
无NULL指针:循环链表中没有指向NULL的指针,这简化了某些操作的终止条件判断。
灵活遍历:从链表的任何一个节点出发,都可以遍历到链表中的所有节点。
空间效率:与单链表和双向链表相比,循环链表不需要额外的存储空间,只是通过改变链表的链接方式来实现其特性。
应用场景:循环链表常用于实现一些需要循环访问链表元素的算法,如约瑟夫问题(Josephus Problem)等。
5.双向循环链表的基本操作
以带头结点的双向循环链表为例
双向循环链表中节点类型的描述如下
typedef int DLTDatatype; typedef struct ListNode { DLTDatatype data; //数据域 struct ListNode* next;// 前驱指针域,指向前一个节点 struct ListNode* prev;// 后继指针域,指向后一个节点 }ListNode;
1.初始化
ListNode* InitList() {
ListNode*phead = CreateListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
2.创建节点
ListNode* CreateListNode(DLTDatatype x) {
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL) {
perror("malloc false");
return NULL;
}
newnode->next = NULL;
newnode->prev = NULL;
newnode->data = x;
return newnode;
}
3.遍历
void PrintList(ListNode* phead) {
assert(phead);
ListNode* cur = phead->next;
printf("head<=>");
while (cur != phead) {
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
4.判断是否为空
bool IsEmpty(ListNode* phead) {
assert(phead);
return phead->next == phead;
}
5.查找
ListNode*FindNode(ListNode* phead, DLTDatatype x) {
assert(phead);
ListNode* cur = phead->next;
while (cur != phead) {
if (cur->data == x) {
return cur;
}
cur = cur->next;
}
return NULL;
}
6.尾插
void pushrear(ListNode* phead, DLTDatatype x) {
assert(phead);
ListNode* newnode = CreateListNode(x);
ListNode* tail = phead->prev;
newnode->next = phead;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
/*Insert(phead, phead, x);*/
}
7.尾删
void poprear(ListNode* phead) {
assert(phead);
assert(!IsEmpty(phead));
ListNode* tail = phead->prev;
ListNode* tailprev = tail->prev;
tailprev->next = phead;
phead->prev = tailprev;
free(tail);
tail = NULL;
/*Earse(phead, phead->prev);*/
}
8.头插
void pushfront(ListNode* phead, DLTDatatype x) {
assert(phead);
ListNode* newnode = CreateListNode(x);
ListNode* tailnext = phead->next;
newnode->next = tailnext;
tailnext->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
/*Insert(phead, phead->next, x);*/
}
9.头删
void popfront(ListNode* phead) {
assert(phead);
assert(!IsEmpty(phead));
ListNode* tail = phead->next;
ListNode* tailnext = tail->next;
phead->next = tailnext;
tailnext->prev = phead;
free(tail);
tail = NULL;
/*Earse(phead, phead->next);*/
}
10.在pos节点前面插入节点
void Insert(ListNode* phead, ListNode* pos, DLTDatatype x) {
assert(phead);
assert(pos);
ListNode* per = pos->prev;
ListNode* newnode = CreateListNode(x);
per->next = newnode;
newnode->prev = per;
newnode->next = pos;
pos->prev = newnode;
}
11.删除pos节点
void Earse(ListNode* phead, ListNode* pos) {
assert(phead);
assert(pos);
ListNode* pre = pos->prev;
ListNode* cur = pos->next;
pre->next = cur;
cur->prev = pre;
free(pos);
}
12.销毁
void Destroy(ListNode* phead) {
assert(phead);
ListNode* cur = phead->next;
ListNode* t;
while (cur != phead) {
t = cur->next;
free(cur);
cur = t;
}
free(phead);
}
13.测试用例
ps:以下测试用例仅供参考,可以尝试带入其他的数值或调用其他的函数来测试
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
void test1() {
ListNode*plist = InitList(); //初始化链表plist
pushrear(plist, 1); //用尾插法依次插入1 2 3 4
pushrear(plist, 2);
pushrear(plist, 3);
pushrear(plist, 4);
PrintList(plist); //遍历打印链表
poprear(plist); //删除尾节点
poprear(plist); //删除尾节点
PrintList(plist); //遍历打印删除后的链表
Destroy(plist); //销毁链表
plist=NULL; //plist置为空
}
void test2() {
ListNode* plist = InitList(); //初始化链表plist
pushfront(plist, 1); //用头插法依次插入1 2 3 4
pushfront(plist, 2);
pushfront(plist, 3);
pushfront(plist, 4);
PrintList(plist); //遍历打印链表
ListNode* ret = FindNode(plist, 3);//查找值为3的节点ret
Insert(plist, ret, 5); //在ret节点前插入值为5的节点
PrintList(plist); //遍历打印插入后的链表
Earse(plist, ret); //删除ret节点
ret = NULL; //ret节点置为空
PrintList(plist); //遍历打印删除后的链表
Destroy(plist); //销毁链表
plist = NULL; //plist置为空
}
int main() {
test1();
test2();
return 0;
}
测试结果如下:
四.顺序表和链表的比较
顺序表和链表是两种基本的数据结构,它们都属于线性表的一种,但在存储方式、操作效率、应用场景等方面存在明显的差异。下面从几个方面对顺序表和链表进行比较:
存储空间:
- 顺序表:在逻辑和物理空间上都是连续的,它使用底层逻辑数组来存储数据,数组中的元素被存储在一段连续的内存空间中。
- 链表:在逻辑上是连续的,但在物理空间上不一定是连续的。链表由一个个节点组成,每个节点包含数据域和指针域(或链接域),节点之间通过指针相互连接。
随机访问性:
- 顺序表:支持随机访问,即可以通过下标直接访问表中的任意元素,时间复杂度为O(1)。
- 链表:不支持随机访问,要访问某个元素,需要从头节点开始遍历链表,直到找到该元素,时间复杂度为O(n)。
插入和删除操作:
- 顺序表:在中间的某个位置插入或删除元素时,需要移动多个元素,效率较低,时间复杂度为O(n)。
- 链表:在中间的某个位置插入或删除元素时,只需要改变指针的指向,效率较高,时间复杂度为O(1)(在已知节点位置的情况下)。
容量和扩容:
- 顺序表:在插入元素时,如果容量不够,需要扩容,扩容本身存在消耗,还可能造成空间浪费。
- 链表:在插入元素时,按需申请空间,不存在容量和空间浪费的问题,但每个元素在插入前都需要申请新的空间。
应用场景:
- 顺序表:适用于需要快速访问指定位置的数据的场景,如数组、栈等。
- 链表:适用于需要频繁在任意位置插入或删除元素的场景,如队列、图的邻接表等。
缓存利用率:
- 顺序表:由于数据在内存中连续存放,调用一次顺序表中的数据时,可能大部分数据都被调入Cache,缓存利用率高。
- 链表:数据在内存中随机存放,访问链表中的其他元素时可能需要多次将节点附近的数据调入Cache,缓存利用率低,还可能造成缓存污染。
综上所述,顺序表和链表各有优缺点,在实际应用中应根据具体需求选择合适的数据结构。需要注意的是,以上比较是基于一般情况的,不同编程语言和实现方式可能会对性能产生一定影响。
续的,它使用底层逻辑数组来存储数据,数组中的元素被存储在一段连续的内存空间中。