1. 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2. 顺序表
2.1概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表有两种:
1、静态顺序表:使用定长数组存储元素。
2. 动态顺序表:使用动态开辟的数组存储。
2.2静态和动态的区别
静态顺序表和动态顺序表都是存储数据的线性结构,但它们在实现方式和特点上有一些区别。
1、静态顺序表是在程序运行前就确定了大小的数组,数组的大小是固定的,不能动态改变。
2、动态顺序表则是使用动态分配的内存空间来存储数据,可以根据需要动态改变大小。
3、静态顺序表的优点是实现简单,内存占用固定,操作速度较快。缺点是大小固定,无法动态调整,可能会造成内存浪费或者空间不足的问题。
4、动态顺序表的优点是大小可以动态调整,节省内存空间,缺点是实现相对复杂,操作速度可能较慢。
2.3动态顺序表的实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间
大小,所以下面我们实现动态顺序表。
1、接口
#pragma once // 防止头文件被重复包含
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int SLDataType; // 定义数据类型SLDataType为int
typedef struct SeqList // 定义结构体SeqList
{
SLDataType* a; // 指向SLDataType类型的指针a
int size; // 表示当前顺序表中有效元素的个数
int capacity; // 表示顺序表的容量
}SeqList;
void SeqListInit(SeqList* ps); // 初始化顺序表
void SeqListDestroy(SeqList* ps); // 销毁顺序表
void SeqListPrint(SeqList* ps); // 打印顺序表
void SeqListCheckCapacity(SeqList* ps); // 检查顺序表容量
void SeqListPushBack(SeqList* ps,SLDataType x); // 在顺序表尾部插入元素
void SeqListPushFront(SeqList* ps,SLDataType x); // 在顺序表头部插入元素
void SeqListPopBack(SeqList* ps); // 删除顺序表尾部元素
void SeqListPopFront(SeqList* ps); // 删除顺序表头部元素
int SeqListFind(SeqList* ps,SLDataType x); // 查找元素在顺序表中的位置
void SeqListInsert(SeqList* ps,int pos,SLDataType x); // 在指定位置插入元素
void SeqListErase(SeqList* ps,int pos); // 删除指定位置的元素
2、接口实现
//初始化顺序表
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->a = NULL; //数组指针初始化为空
ps->size = 0; //顺序表大小初始化为0
ps->capacity = 0; //顺序表容量初始化为0
}
//销毁顺序表
void SeqListDestroy(SeqList* ps)
{
assert(ps);
if (ps->a != NULL) //如果数组指针不为空
{
free(ps->a); //释放数组空间
ps->a = NULL; //数组指针置为空
ps->size = ps->capacity = 0; //顺序表大小和容量都置为0
}
}
//打印顺序表
void SeqListPrint(SeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++) //遍历顺序表
{
printf("%d ", ps->a[i]); //打印元素
}
printf("\n"); //换行
}
//检查顺序表容量是否足够
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
if (ps->size == ps->capacity) //如果顺序表已满
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; //计算新的容量
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * newcapacity); //重新分配空间
if (tmp == NULL) //如果分配失败
{
perror("realloc fail"); //输出错误信息
return;
}
ps->a = tmp; //更新数组指针
ps->capacity = newcapacity; //更新容量
tmp = NULL; //释放临时指针
free(tmp); //释放临时指针
}
}
//在顺序表尾部插入元素
void SeqListPushBack(SeqList* ps,SLDataType x)
{
assert(ps);
SeqListCheckCapacity(ps); //检查容量是否足够
ps->a[ps->size] = x; //插入元素
ps->size++; //更新大小
}
//在顺序表头部插入元素
void SeqListPushFront(SeqList* ps,SLDataType x)
{
assert(ps);
SeqListCheckCapacity(ps); //检查容量是否足够
for (size_t i = ps->size; i > 0; i--) //从后往前遍历
{
ps->a[i] = ps->a[i - 1]; //后面的元素依次往后移动
}
ps->a[0] = x; //插入元素
ps->size++; //更新大小
}
//在顺序表尾部删除元素
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size>0); //如果顺序表为空,程序终止
ps->size--; //更新大小
}
//在顺序表头部删除元素
void SeqListPopFront(SeqList*ps)
{
assert(ps);
assert(ps->size > 0); //如果顺序表为空,程序终止
for (size_t i = 0; i < ps->size - 1; i++) //从前往后遍历
{
ps->a[i] = ps->a[i + 1]; //前面的元素依次往前移动
}
ps->size--; //更新大小
}
//查找元素在顺序表中的位置
int SeqListFind(SeqList*ps,SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++) //遍历顺序表
{
if (x == ps->a[i]) //如果找到了
return i; //返回位置
}
return -1; //如果没找到,返回-1
}
//在指定位置插入元素
void SeqListInsert(SeqList* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size); //如果插入位置不合法,程序终止
SeqListCheckCapacity(ps); //检查容量是否足够
for (int i = ps->size; i > pos; i--) //从后往前遍历
{
ps->a[i] = ps->a[i-1]; //后面的元素依次往后移动
}
ps->a[pos] = x; //插入元素
//更新大小 ps->size++; }
//删除指定位置的元素
void SeqListErase(SeqList* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size); //如果删除位置不合法,程序终止
for (int i = pos; i < ps->size - 1; i++) //从前往后遍历
{
ps->a[i] = ps->a[i + 1]; //前面的元素依次往前移动
}
ps->size--; //更新大小
}
3. 链表
3.1 链表的概念及结构
概念:
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
物理结构上:
3.2 链表的分类
1.单向或双向链表
2. 带头或不带头链表
3. 循环或非循环链表
实际中最常用的两种:
1. 无头单向非循环链表:
结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:
结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。
3.3 单链表
1、接口
#pragma once
#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);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 单链表在指定位置插入结点
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDateType x);
// 单链表删除指定位置的结点
void SLTErase(SListNode** pplist, SListNode* pos);
// 销毁单链表
void SLTDestroy(SListNode** pplist);
2、接口实现
// 打印单链表
void SListPrint(SListNode*phead)
{
SListNode* cur = phead;
while (cur!=NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
// 创建新的节点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
// 在单链表尾部插入节点
void SListPushBack(SListNode** pplist, SLTDateType x)
{
assert(pplist);
SListNode* newndoe = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newndoe;
}
else
{
SListNode* cur = *pplist;
while (cur->next!=NULL)
{
cur=cur->next;
}
cur->next = newndoe;
}
}
// 在单链表头部插入节点
void SListPushFront(SListNode** pplist, SLTDateType x)
{
assert(pplist);
SListNode* newnode = BuySListNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
// 删除单链表尾部节点
void SListPopBack(SListNode** pplist)
{
assert(pplist && (*pplist));
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* tail = *pplist;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
// 删除单链表头部节点
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* temp = *pplist;
*pplist = (*pplist)->next;
free(temp);
}
// 查找节点的值
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
assert(plist);
SListNode* cur = plist;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
// 在指定位置之前插入节点
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{
assert(pplist);
assert(*pplist);
assert(pos);
if (*pplist == pos)
{
SListPushFront(pplist, x);
}
else
{
SListNode* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
// 在指定位置之后插入节点
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next=newnode;
}
// 删除指定位置的节点
void SLTErase(SListNode** pplist, SListNode* pos)
{
assert(pplist);
assert(*pplist);
assert(pos);
if (*pplist == pos)
{
SListPopFront(pplist);
}
else
{
SListNode* prev = *pplist;
while (prev->next!=pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
// 删除指定位置之后的节点
void SListEraseAfter(SListNode* pos)
{
assert(pos);
assert(pos->next);
SListNode* temp = pos->next;
pos->next = pos->next->next;
free(temp);
temp = NULL;
}
// 销毁单链表
void SLTDestroy(SListNode** pplist)
{
assert(pplist);
SListNode* cur = *pplist;
while (cur)
{
SListNode* temp = cur->next;
free(cur);
cur = temp;
}
*pplist = NULL;
}
在上面的代码中有两个值得我们思考的问题:
1、为什么不在pos位置之前插入?
这是因为在单链表中,如果要在指定位置之前插入节点,需要知道指定位置之前的节点,而单链表的节点只能通过next指针找到下一个节点,无法直接找到前一个节点。因此,在单链表中,通常会选择在指定位置之后插入节点,或者通过其他方式重新组织链表来实现在指定位置之前插入节点的操作。
2、为什么不删除pos位置?
在单链表中,如果要删除指定位置的节点,需要知道指定位置之前的节点,以便修改前一个节点的next指针来跳过当前节点,从而实现删除操作。由于单链表的节点只能通过next指针找到下一个节点,无法直接找到前一个节点,因此删除指定位置的节点比较麻烦,需要遍历链表找到指定位置之前的节点。而删除指定位置之后的节点则相对容易,只需修改当前节点的next指针即可。因此,通常在单链表中会选择删除指定位置之后的节点,或者通过其他方式重新组织链表来实现删除指定位置的节点的操作。
3.4 带头双向循环链表
1、接口
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
//初始化链表
ListNode* ListInit();
// 创建返回链表的头结点.
ListNode* ListCreate(LTDataType x);
// 双向链表销毁
void ListDestroy(ListNode** pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
2、接口实现
// 初始化链表
ListNode* ListInit()
{
// 创建头节点
ListNode* head = (ListNode*)malloc(sizeof(ListNode));
if (head == NULL)
{
perror("malloc fail 01");
return NULL;
}
// 头节点的前驱和后继都指向自己,数据域设为-1
head->_prev = head;
head->_next = head;
head->_data = -1;
return head;
}
// 创建节点
ListNode* ListCreate(LTDataType x)
{
// 申请新节点
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc fail 02");
return NULL;
}
// 初始化新节点的前驱、后继和数据域
newnode->_prev = NULL;
newnode->_next = NULL;
newnode->_data = x;
return newnode;
}
// 打印链表
void ListPrint(ListNode* pHead)
{
if (pHead == NULL)
{
printf("NULL");
}
else
{
ListNode* cur = pHead->_next;
while (cur != pHead)
{
printf("%d - ", cur->_data);
cur = cur->_next;
}
}
printf("\n");
}
// 销毁链表
void ListDestroy(ListNode** pHead)
{
assert(*pHead); // 断言头节点不为空
ListNode* cur = (*pHead)->_next;
while (cur != *pHead)
{
ListNode* next = cur->_next;
free(cur);
cur = next;
}
free(*pHead);
*pHead = NULL;
}
// 尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead); // 断言头节点不为空
ListNode* node = ListCreate(x);
node->_prev = pHead->_prev;
node->_next = pHead;
pHead->_prev->_next = node;
pHead->_prev = node;
}
// 尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead); // 断言头节点不为空
assert(pHead->_next != pHead); // 断言链表不为空
ListNode* delnode = pHead->_prev;
delnode->_prev->_next = pHead;
pHead->_prev = delnode->_prev;
free(delnode);
delnode = NULL;
}
// 头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead); // 断言头节点不为空
ListNode* node = ListCreate(x);
node->_prev = pHead;
node->_next = pHead->_next;
pHead->_next->_prev = node;
pHead->_next = node;
}
// 头删
void ListPopFront(ListNode* pHead)
{
assert(pHead); // 断言头节点不为空
ListNode* delnode = pHead->_next;
delnode->_next->_prev = pHead;
pHead->_next = delnode->_next;
free(delnode);
delnode = NULL;
}
// 查找节点
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead); // 断言头节点不为空
ListNode* cur = pHead->_next;
while (cur != pHead)
{
if (cur->_data == x)
{
return cur;
}
cur = cur->_next;
}
return NULL;
}
// 指定位置之前插入节点
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos); // 断言位置节点不为空
ListNode* node = ListCreate(x);
node->_next = pos;
node->_prev = pos->_prev;
pos->_prev->_next = node;
pos->_prev = node;
}
// 删除指定位置的节点
void ListErase(ListNode* pos)
{
assert(pos); // 断言位置节点不为空
pos->_next->_prev = pos->_prev;
pos->_prev->_next = pos->_next;
free(pos);
pos = NULL;
}
4. 顺序表和链表的区别
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存命中率 | 高 | 低 |