●🧑个人主页:你帅你先说.
●📃欢迎点赞👍关注💡收藏💖
●📖既选择了远方,便只顾风雨兼程。
●🤟欢迎大家有问题随时私信我!
●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。
1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2.顺序表
2.1概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储元素。
- 动态顺序表:使用动态开辟的数组存储。
2.2接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
typedef int SLDataType;
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组
size_t size ; // 有效数据个数
size_t capicity ; // 容量空间的大小
}SeqList
2.2.1初始化顺序表
void SeqListInit(SL* ps)
{
ps->a = NULL;
ps->size = ps->capacity = 0;
}
2.2.2打印顺序表
void SeqListPrint(SL* ps)
{
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
2.2.3检查空间余量
void SeqListCheckCapacity(SL* ps)
{
// 如果没有空间或者空间不足,那么我们就扩容
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity*sizeof(SLDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapacity;
}
}
2.2.4顺序表尾插
void SeqListPushBack(SL* ps, SLDataType x)
{
SeqListCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
2.2.5顺序表尾删
void SeqListPopBack(SL* ps)
{
assert(ps->size > 0);
ps->size--;
}
2.2.6顺序表头插
void SeqListPushFront(SL* ps, SLDataType x)
{
SeqListCheckCapacity(ps);
// 挪动数据
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[0] = x;
ps->size++;
}
2.2.7顺序表头删
void SeqListPopFront(SL* ps)
{
assert(ps->size > 0);
// 挪动数据
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++ begin;
}
ps->size--;
}
2.2.8查找下标
int SeqListFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
2.2.9指定位置插入
void SeqListInsert(SL* ps, int pos, SLDataType x)
{
assert(pos >= 0 && pos <= ps->size);
SeqListCheckCapacity(ps);
// 挪动数据
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
2.2.10指定位置删除
void SeqListErase(SL* ps, int pos)
{
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
2.2.11顺序表销毁
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
3.单链表
3.1概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
这是单链表的逻辑结构,是我们假想出来的,方便我们理解。而链表的物理结构也就是真实的样子其实是这样的。
链表的分类
1.单向或者双向
2.带头或者不带头
3.循环或者非循环
虽然链表的结构很多,但实际中我们用的最多的还是无头单向非循环链表
和带头双向循环链表
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向 循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而 简单了,后面我们代码实现了就知道了。
3.2接口实现
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SLTNode;
从上面那张图我们知道单链表是通过存下一个结点的地址来进行访问的。
所以单链表要访问下一个结点的操作是这样的
SLTNode* Node;
Node = Node->next;
搞清楚了这个,接下来的接口实现你才能理解
3.2.1创建结点
SLTNode* BuyListNode(SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
3.2.2单链表尾插
void SListPushBack(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
3.2.3单链表尾删
void SListPopBack(SLTNode** pphead)
{
assert(*pphead != NULL);
// 1、一个节点
// 2、两个及以上节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
3.2.4单链表头插
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
3.2.5单链表头删
void SListPopFront(SLTNode** pphead)
{
//if (*pphead == NULL)
// return;
assert(*pphead != NULL);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
3.2.6单链表打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
3.2.7寻找结点
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
3.2.8指定位置前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x);
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
}
else
{
// 找到pos的前一个位置
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
}
3.2.9指定位置后插入
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
assert(pos);
SLTNode* newnode = BuyListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
3.2.10指定位置删除
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
3.2.11指定位置后删除
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
3.2.12单链表销毁
void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
4.带环问题
我们先来看道题
这道题的基本思路是快慢指针
,即刚开始定义一个slow指针和一个fast指针,先让他们都指向头结点,然后快指针一次走两步,慢指针一次走一步,若快指针等于慢指针,则带环,若在快指针等于NULL或快指针的next域等于NULL之前都不等于慢指针,则说明不带环。
代码实现:
bool hasCycle(struct ListNode *head)
{
struct ListNode * fast,*slow;
fast = slow = head;
while(fast&&fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(slow==fast)
{
return true;
}
}
return false;
}
延伸问题:
1.为什么slow和fast一定会在环中相遇?会不会在环中错过,永远遇不上?请你证明一下
2.能不能fast一次走n步(n>2)?请你证明一下
证:
首先,slow和fast,fast一定先进环,这时slow走了入环前距离的一半。其次,随着slow进环,fast已经在环里面走了一段,走了多少跟环的大小有关系,假设slow进环的时候,slow跟fast的距离是N,fast开始追slow,slow每次往前走1步,fast往前走2步,每追一次,判断一下相遇,每追1次,fast和slow的距离变化:
N
N-1
N-2
N-3
…
1
0
每追一次,距离少1,他们之间的距离最后减到0的时候就是相遇的点。
那可不可能在环中错过呢?
假设slow一次走一步,fast一次走3步,slow进环以后,fast跟slow之间的距离为N,fast开始追slow,他们之间的距离变化如下:
N是偶数
N
N-2
N-4
…
2
0
可以追上
N是奇数
N
N-2
N-4
…
1
-1
N等于-1意味着他们之间的距离变为C-1(C是环的长度),如果C-1是奇数,那么就永远追不上了,如果C-1是偶数,那么就可以追上。
其它情况依次类推
3.slow走一步,fast走两步,一定会相遇。如何求环的入口点呢?
通过刚刚上面的推理我们知道,在slow进入环前,fast可能已经在环里走了几圈了
所以此时我们可以得出快慢指针距离的表达式
慢指针走的距离:L+X
快指针走的距离:L+N*C+X
因为快指针走的距离是慢指针的两倍,所以我们可以得出以下的等式
2*(L+X) = L+N*C+X
L+X = N*C
L = N*C-X
L = (N-1)*C+C-X
5.双链表
5.1概念
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向 循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而 简单了,后面我们代码实现了就知道了。
5.2接口实现
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
5.2.1双链表的初始化和创建结点
LTNode* ListInit()
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->next = phead;
phead->prev = phead;
return phead;
}
LTNode* BuyListNode(LTDateType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
5.2.2打印双链表
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
5.2.3双链表尾插
void ListPushBack(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* newnode = BuyListNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
5.2.4双链表尾删
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
free(tail);
tailPrev->next = phead;
phead->prev = tailPrev;
}
5.2.5双链表头插
void ListPushFront(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* next = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;
}
5.2.6双链表头删
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* next = phead->next;
LTNode* nextNext = next->next;
phead->next = nextNext;
nextNext->prev = phead;
free(next);
}
5.2.7双链表查找结点
LTNode* ListFind(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
5.2.8双链表任意位置之前插入
void ListInsert(LTNode* pos, LTDateType x)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* newnode = BuyListNode(x);
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
5.2.9双链表任意位置删除
void ListErase(LTNode* pos)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
5.2.10双链表销毁
void ListDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;
}
在销毁完双链表后别忘了要把指针置空,函数内置空形参不影响实参。
学完了任意位置之前插入
和任意位置删除
这两个接口实现后,我们就可以对上面的头插头删和尾插尾删进行优化了。
头插
void ListPushBack(LTNode* phead, LTDateType x)
{
assert(phead);
ListInsert(phead, x);
}
尾插
void ListPushFront(LTNode* phead, LTDateType x)
{
assert(phead);
ListInsert(phead->next, x);
}
头删
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
ListErase(phead->next);
}
尾删
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
ListErase(phead->prev);
}
你会发现,这两个接口实现后,整个双链表的接口实现就变得非常的简洁,这也是由双链表的结构优势所决定的。
6.顺序表和链表的区别
在数据结构中,任何一种结构都有它存在的价值,每种结构都有它的优势和劣势,没有谁比谁高贵,所以顺序表和链表各有优缺点,严格来说,顺序表和链表是相辅相成的两个结构。
顺序表
优点:
1.支持随机访问,需要随机访问结构支持算法的可以很好适用。
2.cpu高速缓存利用率更高。
缺点:
1.头部中部插入删除时间效率低。
2.连续的物理空间,空间不够了以后需要增容
i)增容有一定程序的消耗。
ii)为了避免频繁增容,一般我们都按倍数去增,用不完可能存在一定的空间浪费。
链表(双向带头循环链表)
优点:
1.任意位置插入删除效率高,为O(1)。
2.按需申请释放空间。
缺点:
1.不支持随机访问,也就意味着一些排序,二分查找等在这种结构上不适用。
2.链表存储一个值,同时要存储链接指针,也有一定的消耗。
3.cpu高速缓存命中率更低。
这样的文章你还不快 点赞👍关注💡收藏⭐
📢:悄悄告诉你长按⭐️可一键三连!