目录
一、线性表总述
1.线性结构的特点:
(1)存在唯一一个被称作是“第一个”的数据元素;
(2)存在唯一一个被称作是“最后一个”的数据元素;
(3)除第一个以外,集合中的每个数据元素均只有一个前驱;
(4)除最后一个以外,集合中的每一个元素均只有一个后继;
2.一个线性表是n个数据元素的有限序列;在较为复杂的线性表中,一个数据元素可以由若干个数据项组成;常把数据元素成为记录,含有大量记录的线性表又称文件。
3.同一线性表中的数据元素必有相同特性。
二、顺序表
1.线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素。
2.线性表的顺序存储结构是一种随机存取的存储结构:只要确定了存储线性表的起始位置,线性表中任意数据元素都可以随机存取。
3.线性表的顺序存储结构,逻辑上相邻的数据元素在物理位置上也是相邻的。
4.线性表的顺序存储结构相关操作实现,以动态顺序表为例:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stddef.h>
#include<stdbool.h>
//定义动态顺序表结构体
typedef int ElemType;
typedef struct SeqList
{
ElemType* data;
int capacity;
int size;
}SeqList;
//实现动态顺序表的基本操作
//顺序表的初始化
void InitList(SeqList* ps)
{
assert(ps);
ps->data = (ElemType*)malloc(sizeof(ElemType) * 5);//初始顺序表中有5个数组空间;
if (NULL == ps->data)
{
printf("空间申请失败\n");
exit(0);//退出系统
}
ps->capacity = 5;
ps->size = 0;
}
//顺序表的销毁
void DestoryList(SeqList* ps)
{
assert(ps);
free(ps->data);
ps->data = NULL;
ps->capacity = 0;
ps->size = 0;
}
//顺序表的判空
bool ListEmpty(SeqList* ps)
{
return ps->size == 0 ? true : false;
}
//取得顺序表的长度
int ListLength(SeqList* ps)
{
return ps->size;
}
//顺序表的扩容
void CapacityCheck(SeqList* ps)
{
if(ps->capacity == ps->size)
{
ps->capacity = ps->capacity * 2;
ElemType* newNode = (ElemType*)realloc(ps->data, sizeof(ElemType) * (ps->capacity));
if (newNode == NULL)
{
printf("顺序表扩容失败\n");
exit(0);
}
ps->data = newNode;
}
}
//打印顺序表中的内容
void SeqListPrint(SeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->data[i]);
}
printf("\n");
}
//顺序表的尾插法
void SeqListPushBack(SeqList* ps, ElemType e)
{
assert(ps);
CapacityCheck(ps);
ps->data[ps->size] = e;
ps->size++;
}
//顺序表的头插法
void SeqListPushFront(SeqList* ps, ElemType e)
{
assert(ps);
CapacityCheck(ps);
for (int i = ps->size; i > 0; --i)
{
ps->data[i] = ps->data[i - 1];
}
ps->data[0] = e;
ps->size++;
}
//顺序表的头删法
void SeqListPopFront(SeqList* ps)
{
assert(ps);
if (ListEmpty(ps))
{
return;
}
for (int i = 0; i < ps->size; ++i)
{
ps->data[i] = ps->data[i + 1];
}
ps->data[ps->size - 1] = 0;
ps->size--;
}
//顺序表的尾删法
void SeqListPopBack(SeqList* ps)
{
assert(ps);
if (ListEmpty(ps))
{
return;
}
ps->data[ps->size - 1] = 0;
ps->size--;
}
// 顺序表查找
int SeqListFind(SeqList* ps, ElemType e)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
if (e == ps->data[i])
{
return i+1;
}
}
return -1;
}
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, ElemType e)
{
assert(ps);
if (pos > ps->size+1 || pos < 1) return;
CapacityCheck(ps);
for (int i = ps->size; i > pos - 1; --i)
{
ps->data[i] = ps->data[i - 1];
}
ps->data[pos - 1] = e;
ps->size++;
}
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, size_t pos)
{
assert(ps);
if (ListEmpty(ps))
{
return;
}
if (pos<1 || pos>ps->size) return;
for (int i = pos - 1; i < ps->size; ++i)
{
ps->data[i] = ps->data[i + 1];
}
ps->data[ps->size - 1] = 0;
ps->size--;
}
三、单链表
1.线性表的链式存储结构是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以连续也可以不连续)。
2.对数据元素来说,不仅要存储其本身的信息外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素的存储映像,成为结点。它包括两个域:存储数据元素信息的域成为数据域;存储其直接后继位置的域成为指针域。
3.线性链表存储结构整个链表的存取必须从头指针开始进行,头指针指示链表的第一个结点的存储位置。由于最后一个数据元素没有直接后继,则线性链表的最后一个结点的指针未“NULL”。
4.单链表可由头指针唯一确定。
5.我们可以在单链表的第一个结点之前附设一个结点,称之为“头结点”,头结点的指针域可以不存储任何信息,也可以存储线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。单链表的头指针指向头结点。
6.线性表的链式表示相关操作实现:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
if (NULL == newNode)
{
printf("空间申请失败\n");
exit(0);
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
// 单链表打印
void SListPrint(SListNode* plist)
{
SListNode* cur = plist;
while (cur)
{
printf("%d-->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
assert(*pplist);
SListNode* cur = *pplist;
if (NULL == cur)
{
cur = BuySListNode(x);
}
else
{
while (cur->next)
{
cur = cur->next;
}
SListNode* temp = BuySListNode(x);
cur->next = temp;
}
}
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
assert(*pplist);
SListNode* cur = *pplist;
if (NULL == cur)
{
cur = BuySListNode(x);
}
else
{
SListNode* temp = BuySListNode(x);
temp->next = cur;
cur = temp;
}
}
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
assert(*pplist);
SListNode* cur = *pplist;
SListNode* pre = NULL;
if (NULL == cur) return;
else if (cur->next == NULL)
{
free(cur);
cur = NULL;
}
else
{
while (cur->next)
{
pre = cur;
cur = cur->next;
}
free(cur);
cur = NULL;
pre->next = NULL;
}
}
// 单链表头删
void SListPopFront(SListNode** pplist)
{
assert(*pplist);
SListNode* cur = *pplist;
if (NULL == cur) return;
else if (cur->next == NULL)
{
free(cur);
cur = NULL;
}
else
{
*pplist = (* pplist)->next;
free(cur);
cur = NULL;
}
}
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
assert(plist);
if (NULL == plist) return NULL;
else
{
SListNode* cur = plist;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
}
return NULL;
}
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
if (NULL == pos) return;
else
{
SListNode* cur = BuySListNode(x);
cur->next = pos->next;
pos->next = cur;
}
}
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
assert(pos);
if (NULL == pos || NULL == pos->next) return;
else
{
SListNode* cur = pos->next;
pos->next = cur->next;
free(cur);
cur = NULL;
}
}
// 单链表的销毁
void SListDestroy(SListNode** pplist)
{
assert(*pplist);
if (*pplist == NULL) return;
else
{
SListNode* cur = *pplist;
while (cur)
{
*pplist = (*pplist)->next;
free(cur);
cur = *pplist;
}
}
}
四、循环链表
循环链表是线性表另一种形式的链式存储结构,其特点在于最后一个结点的指针域指向头结点,整个链表形成一个环。
在循环链表中,从任一结点出发都可到达表中的其他结点。
循环链表的相关操作与单链表大致相似,差别仅在于算法的循环条件变为当前结点是否为头结点。
五、双向链表
单链表的结点中只有一个用于指示直接后继的指针域,由此,从某一结点出发只能顺指针往后巡查其他结点。若要寻查结点的直接前驱,则需要从表头指针出发,为了克服单链表的单向性缺点,由此引入双向链表。
在双向链表的结点中存在两个指针域,其一指向直接后继,另一指向直接前驱,C语言中结构体定义如下:
typedef struct DuListNode
{
ElemType data; //数据域
struct DuListNode* prior; //存储指向直接前驱的指针的指针域
struct DuListNode* next; //存储指向直接后继的指针的指针域
}DuListNode;
双向链表结点结构:
双向链表的逻辑示意图:
六、双向循环链表
和单链表的循环链表相似,双向链表也有循环表,双向循环链表的逻辑示意图如下:
双向链表相关操作,以带头结点的双向循环链表举例:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 带头+双向+循环链表增删查改实现
typedef int ElemType;
typedef struct ListNode
{
ElemType data;
struct ListNode* next;
struct ListNode* prior;
}ListNode;
//建立双向链表的结点
ListNode* BuyListNode(ElemType e)
{
ListNode* ret = (ListNode*)malloc(sizeof(ListNode));
if (NULL == ret)
{
printf("空间申请失败\n");
exit(0);
}
ret->data = e;
ret->next = NULL;
ret->prior = NULL;
return ret;
}
// 创建返回链表的头结点.
ListNode* ListCreate()
{
ListNode* ret = (ListNode*)malloc(sizeof(ListNode));
if (NULL == ret)
{
printf("空间申请失败\n");
exit(0);
}
ret->prior = ret;
ret->next = ret;
return ret;
}
// 双向链表销毁
void ListDestory(ListNode** pHead)
{
assert(pHead);
ListNode* cur = (*pHead)->next;
while (cur != (*pHead))
{
ListNode* temp = cur->next;
free(cur);
cur = temp;
}
free(*pHead);
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, ElemType e)
{
assert(pHead);
ListNode* cur = pHead->prior;
ListNode* newNode = BuyListNode(e);
newNode->next = pHead;
pHead->prior = newNode;
cur->next = newNode;
newNode->prior = cur;
}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->prior;
ListNode* pre = cur->prior;
pre->next = pHead;
pHead->prior = pre;
free(cur);
cur = NULL;
}
// 双向链表头插
void ListPushFront(ListNode* pHead, ElemType e)
{
assert(pHead);
ListNode* newNode = BuyListNode(e);
ListNode* cur = pHead->next;
newNode->next = cur;
cur->prior = newNode;
pHead->next = newNode;
newNode->prior = pHead;
}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
ListNode* ret = cur->next;
pHead->next = ret;
ret->prior = pHead;
free(cur);
cur = NULL;
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, ElemType x)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
if (x == cur->data)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, ElemType x)
{
assert(pos);
ListNode* pre = pos->prior;
ListNode* newNode = BuyListNode(x);
pre->next = newNode;
newNode->prior = pre;
newNode->next = pos;
pos->prior = newNode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* pre = pos->prior;
ListNode* next = pos->next;
pre->next = next;
next->prior = pre;
free(pos);
pos = NULL;
}
七、顺序表与链表的比较
比较 | 顺序表 | 链表 | |
空间性能 | 存储空间分配 | 1.存储空间必须预先分配2.元素个数扩充受限 3.易造成存储空间浪费或溢出现象 | 不需要预先分配存储空间,只要内存空间允许,链表中的数据元素个数就没有限制。 |
存储密度大小 | 顺序表的存储空间利用率为100% | 链表的每个结点除数据域外还要额外设置指针域,空间利用率不足100% | |
时间性能 | 存储元素效率 | 顺序表可按照数组下标实现对数据元素的随机存取。 | 链表在访问某个数据元素时,只能从表头依次向后遍历链表,直到取到对应位置元素。 |
插入、删除效率 | 插入、删除操作中平均需要移动表中一半的结点,时间开销较大。 | 在确定需要进行插入、删除的位置后,无需移动数据,只需修改指针指向即可。 |
顺序表与链表应用场景总结:
- 当线性表的长度变化较大,难以估计存储规模时,宜采用链表作为存储结构。
- 当线性表的长度变化不大,易于实现确定其大小时,为了节省存储空间,宜采用顺序表作为存储结构。
- 当线性表的主要操作与取值操作相关,很少做插入与删除时,宜采用顺序表作为存储结构。
- 当线性表频繁进行插入与删除操作时,宜采用链表作为存储结构。