**线性表(linear list)**是 n 个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

2、顺序表
2.1 概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
-
静态顺序表:使用定长数组存储。
-
动态顺序表:使用动态开辟的数组存储。(重点)
// 顺序表的静态存储
#define N 100
typedef int SLDataType;
typedef struct SeqList
{
SLDataType array[N]; // 定长数组
size_t size; // 有效数据的个数
}SeqList;
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组
size_t size ; // 有效数据个数
size_t capicity ; // 容量空间的大小
}SeqList;
2.2 接口实现
typedef int SLDataType;
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组
size_t size ; // 有效数据个数
size_t capicity ; // 容量空间的大小
}SeqList;
// 基本增删查改接口
// 顺序表初始化
void SeqListInit(SeqList* psl)
{
assert(pq);
pq->a = NULL;
pq->size = pq->capacity = 0;
}
// 检查空间,如果满了,进行增容
void CheckCapacity(SeqList* psl)
{
if (pq->size == pq->capacity)
{
int newcapacity = (pq->capacity == 0) ? 7 : pq->capacity * 2;
Seqdatatype* newA = (Seqdatatype*)realloc(pq->a, sizeof(Seqdatatype) * newcapacity);
if (newA == NULL)
{
printf("增容失败!\n");
exit(-1);
}
pq->a = newA;
pq->capacity = newcapacity;
}
}
// 顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x)
{
////第一种方法,自己实现
//assert(pq);
//SeqlistCheckCap(pq);
//pq->a[pq->size] = x;
//pq->size++;
//第二种方法就是利用任意位置插入函数来实现特殊情况:pos等于pq->size时候就是尾插
SeqlistInsert(pq, pq->size, x);
}
// 顺序表尾删
void SeqListPopBack(SeqList* psl)
{
////第一种方法是自己实现
//assert(pq);
//assert(pq->size > 0);
//pq->size--;
//第二种方法就是让任意位置删除的pos等于pq->size-1,就是尾删
SeqlistErase(pq, pq->size-1);
}
// 顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x)
{
////第一种方法就是自己实现
//assert(pq);
//SeqlistCheckCap(pq);
//int end = pq->size - 1;
//while (end >= 0)
//{
// pq->a[end + 1] = pq->a[end];
// end--;
//}
//pq->a[0] = x;
//pq->size++;
//第二种方法就是利用任意位置插入函数来实现特殊情况:pos等于0时候就是头插
SeqlistInsert(pq, 0, x);
}
// 顺序表头删
void SeqListPopFront(SeqList* psl)
{
////第一种方法就是自己实现
//assert(pq);
//assert(pq->size > 0);
//int i = 0;
//while (i < pq->size - 1)
//{
// pq->a[i] = pq->a[i + 1];
// i++;
//}
//pq->size--;
//第二种方法就是让任意位置删除的pos等于0,就是头删
SeqlistErase(pq, 0);
}
// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x);
{
assert(pq);
for (int i = 0; i < pq->size; i++)
{
if (pq->a[i] == x)
{
return i;
}
}
return -1;
}
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
assert(pq);
assert(pos >= 0 && pos <= pq->size);
SeqlistCheckCap(pq);
int end = pq->size - 1;
while (end >= pos)
{
pq->a[end + 1] = pq->a[end];
end--;
}
pq->a[pos] = x;
pq->size++;
}
// 顺序表删除pos位置的值
void SeqListErase(SeqList* psl, size_t pos)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
while (pos < pq->size - 1)
{
pq->a[pos] = pq->a[pos + 1];
pos++;
}
pq->size--;
}
// 顺序表销毁
void SeqListDestory(SeqList* psl)
{
assert(pq);
free(pq->a);
pq->a = NULL;
pq->capacity = pq->size = 0;
}
// 顺序表打印
void SeqListPrint(SeqList* psl)
{
assert(pq);
for (int i = 0; i < pq->size; i++)
{
printf("%d ", pq->a[i]);
}
printf("\n");
}
// 顺序表修改
void SeqlistModify(Seqlist* pq, int pos, Seqdatatype x)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
pq->a[pos] = x;
}2.3 顺序表的缺点
- 动态增容有性能消耗
- 头部插入数据时,需要挪动大量的数据,时间复杂度高,效率低
3、链表
3.1 链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
-
单向、双向
-
带头、不带头
-
循环、非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

-
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
-
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
3.2 链表的实现
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
//1、单向 + 无头 + 不循环
typedef struct SListNode
{
int data;
struct SLTNode* next;
}SLTNode;
// 动态申请一个节点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode != NULL)
{
newnode->data = x;
newnode->next = NULL;
return newnode;
}
else
{
printf("空间不够!\n");
exit(-1);
}
}
//销毁单链表
void SListDestroy(SLTNode** pplist)
{
assert(pplist);
SLTNode* cur = *pplist;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pplist = NULL;
}
// 单链表打印
void SListPrint(SLTNode* plist)
{
SLTNode* cur = plist;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
// 单链表的头插
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
// 单链表尾插
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
// 单链表尾删
void SListPopBack(SLTNode** pplist)
{
if (*pplist == NULL)
{
return;
}
else if ((*pplist)->next == NULL)
{
free(*pplist);
(*pplist) = NULL;
}
else
{
SLTNode* tail = *pplist;
SLTNode* prev = NULL;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
// 单链表头删
void SListPopFront(SLTNode** pplist)
{
if (*pplist == NULL)
{
return;
}
//这种累赘一点,多了个判断,其实可以一步到位
/*else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SLTNode* head = *pplist;
(*pplist) = (*pplist)->next;
free(head);
head = NULL;
}*/
else
{
SLTNode* next = (*pplist)->next;
free(*pplist);
*pplist = next;
}
}
// 单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist;
while (cur != NULL)
{
while (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
// 原因:因为需要知道头节点且有多种情况(如pos就是第一个节点等)
// 附:其实前插可以转化为:先后插一个pos节点,然后将原pos节点的值变为x(在不知道头节点的情况下)
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
// 知道头节点的前插
void SListInsertKnowBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
if (*pplist == pos)
{
newnode->next = *pplist;
*pplist = newnode;
}
else
{
SLTNode* cur = *pplist;
newnode->next = pos;
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = newnode;
}
}
// 不知道头节点的前插
void SListInsertUNKnowBefore(SLTNode* pos, SLTDataType x)
{
SListInsertAfter(pos, x);
SLTDataType tmp = pos->data;
SLTNode* next = pos->next;
pos->data = next->data;
next->data = tmp;
}
// pos节点后删
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next1 = pos->next;
pos->next = next1->next;
free(next1);
}
}3.3 链表面试题
-
删除链表中等于给定值 val 的所有节点。OJ链接
-
反转一个单链表。 OJ链接
-
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ链接
-
输入一个链表,输出该链表中倒数第k个结点。 OJ链接
-
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。OJ链接
-
编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。OJ链接
-
链表的回文结构。OJ链接
-
输入两个链表,找出它们的第一个公共结点。OJ链接
-
给定一个链表,判断链表中是否有环。 OJ链接
-
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL OJ链接
-
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深度拷贝。OJ链接
-
对链表进行插入排序。OJ链接
-
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。OJ链接
3.4 双向链表的实现
ListNode* BuyListNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
printf("malloc fail!\n");
exit(-1);
}
newnode->next = NULL;
newnode->prev = NULL;
newnode->x = x;
return newnode;
}
// 创建返回链表的头结点.
ListNode* ListInit()
{
ListNode* phead = BuyListNode(0);//头结点默认赋值给它0
phead->next = phead;
phead->prev = phead;
return phead;
}
// 双向链表销毁
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 ListPrint(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->x);
cur = cur->next;
}
printf("\n");
}
// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{
/*assert(phead);
ListNode* newnode = BuyListNode(x);
ListNode* prev = phead->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = phead;
phead->prev = newnode;*/
ListInsert(phead, x);
}
// 双向链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
/*assert(phead);
ListNode* newnode = BuyListNode(x);
ListNode* next = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;*/
ListInsert(phead->next, x);
}
// 双向链表尾删
void ListPopBack(ListNode* phead)
{
/*assert(phead);
assert(phead->next != phead);
ListNode* del = phead->prev;
ListNode* prev = del->prev;
free(del);
prev->next = phead;
phead->prev = prev;*/
ListErase(phead->prev);
}
// 双向链表头删
void ListPopFront(ListNode* phead)
{
/*assert(phead);
assert(phead->next != phead);
ListNode* del = phead->next;
ListNode* next = del->next;
free(del);
phead->next = next;
next->prev = phead;*/
ListErase(phead->next);
}
// 双向链表查找
ListNode* ListFind(ListNode* phead, LTDataType x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
if (cur->x == x)
return cur;
cur = cur->next;
}
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = BuyListNode(x);
ListNode* prev = pos->prev;
newnode->next = pos;
pos->prev = newnode;
newnode->prev = prev;
prev->next = newnode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
//空返回1,非空返回0
int ListEmpty(ListNode* phead)
{
assert(phead);
return phead->next == phead ? 1 : 0;
}
//返回链表的个数
int ListSize(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
int count = 0;
while (cur != phead)
{
cur = cur->next;
count++;
}
return count;
}4、顺序表和链表的区别
| 不同点 | 顺序表 | 链表 |
|---|---|---|
| 存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
| 随机访问 | 支持:O(1) | 不支持:O(N) |
| 任意位置插入或者删除元素 | 可能需要挪动数据,效率低O(N) | 只需修改指针指向 |
| 插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
| 应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
| 缓存利用率 | 高 | 低 |
