1. 线性表
线性表(linear list)是n个具有相同特色的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常用的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就是说是连续的一条直线。但在物理结构上并不是连续的,线性表在物理上存储时,通常以数组和链表结构的形式存储。
2.顺序表
概念及结构:
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般分为:
1.静态顺序表:使用定长数组存储。
2.动态顺序表:使用动态开辟的数组存储。
接口实现:
静态顺序表适用于与确定知道需要存放多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小。
3.链表
引入:
顺序表在其首部或者任意位置插入或删除时需要搬移大量的元素,但顺序表不适合做大量的插入和删除,原因是其底层是一段连续的空间。
概念:
链表是一种物理存储的结构上非连续,非连续的存储结构,数据结构,数据元素的,逻辑顺序是通过链表的中的指针链接次序实现的。
链表的分类:
对于链表的基本操作中,那些方法需要传递二级指针,那些方法需要传递一级指针?
本质:在函数体中是否需要通过形参指针改变外部实参指针的指向,如果需要,函数的参数必须为二级指针,比如是所有的插入、所有的删除、销毁操作。如果不需要,函数的参数只需要传递一级指针,比如是查找、获取有效的节点的个数。
4.顺序表和链表的区别
共同点: 1.都是线性表 2.元素逻辑存储上是连续的 3.每一个元素都有唯一的前驱以及唯一的后继。
注意: 第一个元素没有前驱,最后一个元素没有后继----循环链表除外。
不同点:
1.底层空间存储方式不同:顺序表底层空间是连续的,链表底层空间不连续
2.插入和删除方式不同:顺序表任意位置插入和删除需要搬移大量元素,效率低,时间复杂度为O(N)
链表任意位置插入和删除不需要搬移元素,效率高,时间复杂度为O(1)
3.随机访问:顺序表底层空间连续,支持随机访问,访问任意位置元素时间复杂度为O(1)
链表底层空间不连续,不支持随机访问,访问任意位置元素时间复杂度为O(N)
4.扩容:顺序表在插入时候需要进行扩容——开辟新空间、拷贝元素、释放旧空间
链表插入时不需要进行扩容
5.空间利用率:一般情况下顺序表空间利用率比较高
链表中元素存储在一个一个的节点中,而每个节点都是malloc出来的。频繁的向内存申请小的内存块–内存碎片、效率低。
6.应用场景:顺序表适合元素的高效存储随机访问的操作比较多的场景
链表适合任意位置的插入和删除比较多的场景
7.缓存利用率:链表的缓存利用率比顺序表低
尾插:
//尾插
void SListPushBack(SListNode** head, SLDataType data)
{
assert(head);
//空链表
//先申请节点
SListNode* newNode = BuySListNode(data);
if (NULL == *head)
{
*head = newNode;
}
else
{
//1 找到链表中的最后一个节点
SListNode* cur = *head;
while (cur->next)
{
cur = cur->next;
//cur++;err
}
//2 插入到新节点
cur->next = newNode;
}
}
尾删:每次删除链表中的最后一个节点
//尾删
void SListPopback(SListNode** head)
{
assert(head);
//1 空链表 直接返回
if (NULL == *head)
return;
//2 只有一个节点
else if (NULL == (*head)->next)
{
free(*head);
*head = NULL;
}
//3 链表非空 - 至少一个节点
else
{
SListNode* cur = *head;
SListNode* prev = NULL;//标记cur的前一个节点
while (cur->next)
{
prev = cur;
cur = cur->next;
}
//最后一个节点已经找到,并删除该节点,同时让cur的前一个的节点指向NULL
prev->next = NULL;
//或者prev->next = cur->next;
free(cur);
}
}
头插:先让0的next指向1,再将head的next指向0,head保存的是头指针的地址,需要改变的是头指针的指向
void SListPushFront(SListNode** head, SLDataType data)
{
assert(head);
SListNode* newNode = BuySListNode(data);
newNode->next = *head;//新节点的next指向原来头节点的地址
*head = newNode;//头指针指向新节点
//1 空链表
//if (NULL == *head)
//{
// *head = newNode;
//}
//
2 链表中有多个节点
//else
//{
// newNode->next = *head;//新节点的next指向原来头节点的地址
// *head = newNode;//头指针指向新节点
//}
}
头删:先标记,在移动,后删除
void SListPopFront(SListNode** head)
{
assert(head);
if (NULL == *head)
return;
SListNode* delNode = *head;
*head = delNode->next;
free(delNode);
//有一个节点,也就是*head->next为空,然后直接free,置位NULL
//else if (NULL == (*head)->next)
//{
// free(*head);
// *head = NULL;
//}
不止一个节点,先标记,再移动,后删除
//else
//{
// SListNode* delNode = *head;
// *head = delNode->next;
// free(delNode);
//}
}
链表中节点的个数: 链表中节点的个数 head指向的就是链表中第一个节点,只需要遍历节点个数,不需要改变头指针的指向
int SListSize(SListNode* head)
{
assert(head);//此处不用assert,因为传过来的是一个链表,空链表是合法的
if (head == NULL)
return 0;
int count = 0;
while (head)
{
count++;
head = head->next;
}
return count;
}
查找数据: 找到返回节点的地址,没找到返回NULL
//查找
SListNode* SListFind(SListNode* head, SLDataType data)
{
SListNode* cur = head;
while (cur)
{
if (cur->data == data)
return cur;
cur = cur->next;
}
return NULL;
}
销毁: 采用头删法将链表销毁
void SListDestroy(SListNode** head)
{
assert(head);
while (*head)
{
SListNode* delNode = *head;
*head = delNode->next;
free(delNode);
}
}
任意位置插入:只能向当前位置的后面进行插入,链表只能向后走
void SListInsertAfter(SListNode* pos, SLDataType data)
{
if (NULL == pos)
return;
SListNode* newNode = BuySListNode(data);
newNode->next = pos->next;
pos->next = newNode;
}
任意位置删除: 任意位置的删除,只能向当前位置的后面进行删除,链表只能向后走
void SListErase(SListNode* pos)
{
if (NULL == pos && pos->next == NULL)
return;
SListNode* delNode = pos->next;
pos->next = delNode->next;
free(delNode);
}