1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储
2.顺序表
概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储元素。
- 动态顺序表:使用动态开辟的数组存储。
接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
typedef int SLDataType;
//动态顺序表
typedef struct SequentialList
{
SLDataType* array; //指向动态开辟的空间
int size; //有效数据个数
int capacity; //容量空间大小
}SeqList;
extern void SLInitialize(SeqList* pc);//顺序表初始化
extern void SLErase(SeqList* pc);//顺序表擦除(销毁)
extern void SLPrintf(SeqList* pc);//打印顺序表
extern void SLPushBack(SeqList* pc, SLDataType x);//顺序表尾插
//pop栈的弹出操作
extern void SLPopback(SeqList* pc);//顺序表尾删
extern void SLPushFront(SeqList* pc, SLDataType x);//顺序表头插
//extern void SLCheckCapacity(SeqList* pc);//检查容量
extern void SLPopFront(SeqList* pc);//顺序表头删
extern int SLFind(SeqList* pc, SLDataType x);// 顺序表查找
extern void SLInsert(SeqList* pc, size_t pos, SLDataType x);// 顺序表在pos位置插入x
extern void SLPosErase(SeqList* pc, size_t pos);// 顺序表删除pos位置的值
定义结构体
typedef
(类型重定义):如果想改变类型,只改变一处整个项目都改了,这样写不是很秒吗?同时也确保了代码的通用性。
typedef int SLDataType;
//动态顺序表
typedef struct SequentialList
{
SLDataType* array; //指向动态开辟的空间
size_t size; //有效数据个数
size_t capacity; //容量空间大小
}SeqList;
我以 定义结构体的方式进行创建接口 ,也可以用结构体指针的方式的进行创建接口,评个人的爱好。
但是我觉得结构体的方式比较方便,如果用了结构体指针的方式还要开辟空间和更改结构体指针指向的地址比较麻烦。
初始化顺序表
void SLInitialize(SeqList* pc)
{
assert(pc != NULL);
pc->array = NULL;
pc->size = 0;
pc->capacity = 0;
}
检查容量
每次满的时候扩2倍比较合理
static void SLCheckCapacity(SeqList* pc)
{
assert(pc != NULL);
if (pc->size == pc->capacity)
{
int NewCapacity = (pc->capacity == 0 ? 4 : pc->capacity * 2);
SLDataType* temp = (SLDataType*)realloc(pc->array, NewCapacity * sizeof(SLDataType));
if (NULL == temp)
{
perror("SLCheckCapacity::temp");
exit(-1);
}
pc->array = temp;
pc->capacity = NewCapacity;
temp = NULL;
}
}
顺序表尾插
//顺序表尾插
void SLPushBack(SeqList* pc, SLDataType x)
{
assert(pc != NULL);
//空间满的情况
SLCheckCapacity(pc);//检查容量
//空间有多余的情况
pc->array[pc->size] = x;
pc->size++;
}
顺序表擦除(销毁)
void SLErase(SeqList* pc)
{
assert(pc != NULL);
if (pc->array != NULL)
{
free(pc->array);
pc->array = NULL;
pc->size = 0;
pc->capacity = 0;
}
}
打印顺序表
void SLPrintf(SeqList* pc)
{
assert(pc != NULL);
int i = 0;
for (i = 0; i < pc->size; i++)
{
printf("%d ", pc->array[i]);
}
printf("\n");
}
顺序表尾删
数据覆盖也是一种删除,但是数据个数必须大于0.
void SLPopback(SeqList* pc)
{
assert(pc != NULL);
//防止删多
assert(pc->size > 0);
pc->size--;
}
顺序表头插
void SLPushFront(SeqList* pc, SLDataType x)
{
assert(pc != NULL);
SLCheckCapacity(pc);//检查容量
//后移动
int i = 0;
for (i = pc->size - 1; i >= 0; i--)
{
pc->array[i + 1] = pc->array[i];
}
pc->array[0] = x;
pc->size += 1;
}
顺序表头删
void SLPopFront(SeqList* pc)
{
assert(pc != NULL);
//防止删多
assert(pc->size != 0);
//前移动
int i = 0;
for (i = 0; i < pc->size - 1; i++)
{
pc->array[i] = pc->array[i + 1];
}
pc->size -= 1;
}
顺序表查找
int SLFind(SeqList* pc, SLDataType x)
{
assert(pc != NULL);
int i = 0;
//找到了返回下标,找不到返回-1
for (i = 0; i < pc->size; i++)
{
if (x == pc->array[i])
{
return i;
}
}
return -1;
}
顺序表在pos位置插入x
void SLInsert(SeqList* pc, size_t pos, SLDataType x)
{
assert(pc != NULL);
assert(pos >= 0);
assert(pos <= pc->size);
SLCheckCapacity(pc);//检查容量
int i = 0;
for (i = pc->size - 1; i > pos; i--)
{
pc->array[i] = pc->array[i + 1];
}
pc->array[pos] = x;
pc->size += 1;
}
顺序表删除pos位置的值
在删除的要保证数据的连续性。
void SLPosErase(SeqList* pc, size_t pos)
{
assert(pc != NULL);
assert(pos >= 0);
assert(pos <= pc->size);
size_t i = 0;
for (i = pos; i < pc->size - 1; i++)
{
pc->array[i] = pc->array[i + 1];
}
pc->size -= 1;
}
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没数据插入了,那么就浪费了95个数据空间。
思考:如何解决以上问题呢?下面给出了链表的结构来看看。
3.链表
链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
注意:
- 从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
- 现实中的结点一般都是从堆上申请出来的
- 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。
链表的分类
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构
无头单向非循环链表
带头双向循环链表
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
链表的实现
无头+单向+非循环链表增删查改实现
typedef int DataType;
struct LinkedList
{
DataType data;
struct LinkedList *next;
};
//单链表打印
extern void PrintfALinkedList(struct LinkedList**ps);
//单链表尾插
extern void ALPushBach(struct LinkedList** pc, DataType x);
//单链表尾删
extern void ALPopBach(struct LinkedList** pc);
//单链表头插
extern void ALpushFront(struct LinkedList** pc,DataType x);
//单链表头删
extern void ALPopFront(struct LinkedList** pc);
//单链表查找
extern struct LinkedList* ALListfind(struct LinkedList** pc, DataType y);
// 单链表在pos位置之后插入x
extern void ALListLnsertAfter(struct LinkedList** pos, DataType x);
// 单链表删除pos位置之后的值
extern void ALListEraseAfter(struct LinkedList** pos);
// 单链表的销毁
extern void ALListErase(struct LinkedList** pc);
单链表申请节点
不管是增加任何结点都需要申请结点所以就创建一个可以公用的函数。
static struct LinkedList* BuyLinkedListNode(DataType x)
{
struct LinkedList* temp = (struct LinkedList *)malloc(sizeof(struct LinkedList));
if (NULL == temp)
{
perror("BuyLinkedListNode::temp");
exit(-1);
}
temp->data = x;
temp->next = NULL;
return temp;
}
单链表尾插
注意:
- 如果链表没有头结点,新结点直接成为头结点。
- 如果有头结点,则需要先找到尾结点,并将尾结点的
next->
指向新结点,新结点的next->
指向NULL
。
尾插是要确保最后一个结点指向空,这是链表的性质。
总结: - 改变
int
,传递int*
给形参,*
形参进行交换改变。 - 改变
int*
,传递int**
给形参,*
形参进行交换改变。
void ALPushBach(struct LinkedList** pc, DataType x)
{
//尾插不需要考虑是否为NULL
struct LinkedList* temp = BuyLinkedListNode(x);
if (NULL == *pc)
{
*pc = temp;
}
else
{
//找尾
struct LinkedList* temp1 = *pc;
while (temp1->next != NULL)
{
temp1 = temp1->next;
}
temp1->next = temp;
}
}
单链表打印
void PrintfALinkedList(struct LinkedList**ps)
{
//空链表也要打印所以不用判断
struct LinkedList* temp = *ps;
while (temp != NULL)
{
printf("%d->", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
单链表尾删
注意:
- 如果
(*pc)->next == NULL
那结点删除的只剩一个了,free
头结点并将头结点制空。 - 找到尾结点,并将尾结点的前一个结点的
next->NULL
,然后将尾结点free
了。
尾删是要确保最后一个结点还指向空,这是链表的性质。
void ALPopBach(struct LinkedList** pc)
{
assert(*pc != NULL);
if ((*pc)->next == NULL)
{
free(*pc);
*pc = NULL;
}
else
{
struct LinkedList* newnode = *pc;
struct LinkedList* temp = NULL;
while (newnode->next != NULL)
{
temp = newnode;
newnode = newnode->next;
}
//temp->next = newnode->next;
//或
temp->next = NULL;
free(newnode);
}
}
单链表头插
注意:
- 如果链表没有头结点,新结点直接成为头结点。
- 如果有头结点,则将新结点的
next->
指向原来的头结点,然后新结点在成为头结点。
void ALpushFront(struct LinkedList** pc,DataType x)
{
struct LinkedList* temp = BuyLinkedListNode(x);
temp->next = *pc;
*pc = temp;
}
单链表头删
void ALPopFront(struct LinkedList** pc)
{
assert(*pc != NULL);
struct LinkedList* temp = *pc;
temp = temp->next;
free(*pc);
*pc = temp;
}
单链表查找
struct LinkedList* ALListfind(struct LinkedList** pc, DataType y)
{
assert(*pc != NULL);
struct LinkedList* temp = *pc;
while (temp != NULL)
{
if (temp->data == y)
{
break;
}
else
{
temp = temp->next;
}
}
return temp;
}
单链表在pos位置之后插入x
void ALListLnsertAfter(struct LinkedList** pos, DataType x)
{
assert(*pos != NULL);
struct LinkedList* newnode = BuyLinkedListNode(x);
struct LinkedList* temp = (*pos)->next;
(*pos)->next = newnode;
newnode->next = temp;
}
单链表删除pos位置之后的值
void ALListEraseAfter(struct LinkedList** pos)
{
assert(*pos != NULL);//空后面的没有节点
assert((*pos)->next != NULL);//节点后面是空删了没意思
struct LinkedList* temp = (*pos)->next;
(*pos)->next = temp->next;
free(temp);
}
单链表的销毁
void ALListErase(struct LinkedList** pc)
{
struct LinkedList* newnode = *pc;
struct LinkedList* temp = NULL;
while (newnode != NULL)
{
temp = newnode;
newnode = newnode->next;
free(temp);
temp = NULL;
}
*pc = NULL;
}
带头+双向+循环链表增删查改实现
这是一种非常完美的链状结构体
typedef int LLDataType;
typedef struct LinkedList
{
LLDataType data;
struct LinkedList* next;
struct LinkedList* previous;
}LList;
//申请结点
extern LList* BuyLListNode(LLDataType x);
//初始化
extern LList* InitializeLList();
//头插
extern void LLPushHead(LList* head,LLDataType x);
//头删
extern void LLPopHead(LList* head);
//打印
extern void PrintfLList(LList* head);
//尾插
extern void LLPushBack(LList* head, LLDataType x);
//尾删
extern void LLPopBack(LList* head);
//查找
extern LList* LListFind(LList* head, LLDataType x);
//在pos之前插入
extern void LListInsert(LList* pos, LLDataType x);
//删除pos位置的值
extern void LListErase(LList* pos);
//释放空间
extern void LListDestroy(LList* head);
申请结点
LList* BuyLListNode(LLDataType x)
{
LList* NewNode = (LList*)malloc(sizeof(LList));
if (NULL == NewNode)
{
perror("BuyLListNode::NewNode");
exit(-1);
}
NewNode->data = x;
NewNode->next = NULL;
NewNode->previous = NULL;
return NewNode;
}
初始化结构体
初始化带头双向循环链表,使哨兵位的两个指针都指向同一块空间0x005A1750
,这样的结构头插尾插都很秒。
用结构体指针接收返回值
LList* InitializeLList()
{
LList* HeadNode = BuyLListNode(-1);
HeadNode->next = HeadNode;
HeadNode->previous = HeadNode;
return HeadNode;
}
头插
void LLPushHead(LList* head,LLDataType x)
{
assert(head != NULL);
LList* NewNode = BuyLListNode(x);
LList* Next = head->next;
NewNode->next = Next;
Next->previous = NewNode;
head->next = NewNode;
NewNode->previous = head;
//LListInsert(head->next, x);//复用函数LListInsert实现头插
}
打印
void PrintfLList(LList* head)
{
assert(head != NULL);
LList* Next = head->next;
while (Next != head)
{
printf("%d ", Next->data);
Next = Next->next;
}
printf("\n");
}
头删
void LLPopHead(LList* head)
{
assert(head != NULL);
assert(head->next != head);//防止链表为空
LList* Next = head->next;
head->next = Next->next;
Next->next->previous = head;
free(Next);
//LListErase(head->next);//复用函数LListErase实现头删
}
尾插
void LLPushBack(LList* head, LLDataType x)
{
assert(head != NULL);
LList* NewNode = BuyLListNode(x);
LList* Back = head->previous;
Back->next = NewNode;
NewNode->previous = Back;
head->previous = NewNode;
NewNode->next = head;
//LListInsert(head, x);//复用函数LListInsert实现尾插
}
尾删
void LLPopBack(LList* head)
{
assert(head != NULL);
assert(head->next != head);//防止为空
LList* Node = head->previous;
LList* previous = Node->previous;
free(Node);
previous->next = head;
head->previous = previous;
//LListErase(head->previous);//复用函数LListErase实现尾删
}
查找
LList* LListFind(LList* head, LLDataType x)
{
assert(head != NULL);
LList* Node = head->next;
while (Node != head)
{
if (Node->data == x)
{
return Node;
}
Node = Node->next;
}
return NULL;
}
在pos之前插入
void LListInsert(LList* pos, LLDataType x)
{
assert(pos != NULL);
LList* NewNode = BuyLListNode(x);
LList* Node = pos->previous;
Node->next = NewNode;
NewNode->previous = Node;
NewNode->next = pos;
pos->previous = NewNode;
}
删除pos位置的值
void LListErase(LList* pos)
{
assert(pos != NULL);
LList* front = pos->previous;
LList* after = pos->next;
free(pos);
front->next = after;
after->previous = front;
}
释放空间
void LListDestroy(LList* head)
{
assert(head != NULL);
LList* node = head->next;
while (node != head)
{
LList* NextNode = node->next;
free(node);
node = NextNode;
}
free(head);
}
4.顺序表和链表的区别和联系
这里的链表是带头+双向+循环链表
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
备注:缓存利用率参考存储体系结构 以及 局部原理性。
学习数据结构最重要的是画图,画图是很重要的,很重要的,很重要的