目录
- [一. 前言](#一. 前言)
- [二. 顺序表](#二. 顺序表)
-
- [1. 顺序表的特点](#1. 顺序表的特点)
- [2. 代码实现](#2. 代码实现)
- [三. 链表](#三. 链表)
-
- [1. 单向链表代码实现](#1. 单向链表代码实现)
- 2.双向链表代码实现
- [四. 顺序表与链表的区别](#四. 顺序表与链表的区别)
- 总结
一. 前言
顺序表和链表是最基础的两种线性表实现方式。它们各有特点,适用于不同的应用场景。本文将详细介绍这两种数据结构的实现原理、C语言代码实现以及它们的优缺点对比。
二. 顺序表
顺序表是用一段连续的物理地址依次存储数据元素的线性结构,采用数组存储。
1. 顺序表的特点
优点:
- 可以通过下标直接访问元素
- 不需要额外的空间存储元素之间的关系
缺点:
- 会造成一定的空间浪费
- 插入删除效率低
2. 代码实现
- 申请空间时,在无法确定空间大小时我们需要动态申请空间。
c
//将int重命名为SLTDatatype
typedef int SLTDatatype;
//顺序表创建一个结构体
typedef struct SeqList
{
SLDataType* arr; //存储数组的指针
int size; //有效个数
int capacity; //最大容量
}SL;
- 初始化和销毁顺序表
c
//初始化顺序表
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//销毁顺序表
void SLDestroy(SL* ps)
{
if(ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
- 空间不足时开辟空间
c
//空间为空时开辟四个空间,不为空空间装满时扩大二倍
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
//增容
SLTDataType* tmp = (SLTDataType*)realloc(ps->arr, newCapacity * sizeof(SLTDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
- 插入数据
尾插:在判断空间足够时直接size位置插入然后size++;
头插:通过循环把元素全部向后移一位,把插入的数据放在下标为0的位置,size++;
c
//尾插
void SLPushBack(SL* ps, SLTDataType x)
{
assert(ps);
//进入函数判断空间是否足够
SLCheckCapacity(ps);
//空间足够在队尾插入数据,把size加一
ps->arr[ps->size++] = x;
}
//头插
void SLPushFront(SL* ps, SLTDataType x)
{
assert(ps);
//进入函数判断空间是否足够
SLCheckCapacity(ps);
//将数据整体向后挪动一位
for (int i = ps->size; i > 0 ; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
//把数据插入在下标为0的位置上
ps->arr[0] = x;
ps->size++;
}
- 删除数据
尾删:通过size--,限制下标访问;
头删:通过循环从下标为1开始向前移动一位,size--;
c
//尾删
void SLPopBack(SL* ps)
{
assert(ps && ps->size);
ps->size--;
}
//头删
void SLPopFront(SL* ps)
{
assert(ps && ps->size);
//数据整体向前挪动一位
for (int i = 0; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
- 查找指定值
c
//通过遍历数组来查找 返回下标
int SLFind(SL* ps, SLTDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
//找到了
return i;
}
}
//未找到
return -1;
}
- 指定位置插入数据
pos位置前插入和pos位置后插入都是通过循环把元素后移然后在指定位置下标插入;
c
//指定位置之前插入数据
//pos为指定位置的下标
void SLInsert(SL* ps, int pos, SLTDataType x)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
//判断空间是否足够
SLCheckCapacity(ps);
//pos及之后数据向后挪动一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
//指定位置之后插入数据
SLInsertAfter(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
//pos之前的数据向后挪动一位
for (int i = ps->size;i > pos+1;i--)
{
ps->arr[i] = ps->arr[i-1];
}
ps->arr[pos+1] = x;
ps->size++;
}
- 删除指定位置的数据
c
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
//pos后面的数据向前挪动一位
for (int i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
- SeqList.h
c
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义动态顺序表的结构
typedef int SLTDataType;
//顺序表创建一个结构体
typedef struct SeqList
{
SLTDataType* arr; //存储数据
int size; //有效数据个数
int capacity; //空间大小
}SL;
//初始化顺序表
void SLInit(SL* ps);
//销毁顺序表
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
//尾插
void SLPushBack(SL* ps, SLTDataType x);
//头插
void SLPushFront(SL* ps, SLTDataType x);
//尾删
void SLPopBack(SL* ps);
//头删
void SLPopFront(SL* ps);
//查找
int SLFind(SL* ps, SLTDataType x);
//指定位置之前插⼊数据
void SLInsert(SL* ps, int pos, SLTDataType x);
//指定位置之后插入数据
void SLInsertAfter(SL* ps, int pos, SLTDataType x);
//删除pos位置的数据
void SLErase(SL* ps, int pos);
- SeqList.c
c
#include"SeqList.h"
//初始化顺序表
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//销毁顺序表
void SLDestroy(SL* ps)
{
if(ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//打印顺序表的数据
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
//空间为空时开辟四个空间,不为空空间装满时扩大二倍
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
//增容
SLTDataType* tmp = (SLTDataType*)realloc(ps->arr, newCapacity * sizeof(SLTDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
//队尾插入数据
void SLPushBack(SL* ps, SLTDataType x)
{
assert(ps);
//进入函数判断空间是否足够
SLCheckCapacity(ps);
//空间足够在队尾插入数据,把size加一
ps->arr[ps->size++] = x;
}
//队头插入数据
void SLPushFront(SL* ps, SLTDataType x)
{
assert(ps);
//进入函数判断空间是否足够
SLCheckCapacity(ps);
//将数据整体向后挪动一位
for (int i = ps->size; i > 0 ; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
//把数据插入在下标为0的位置上
ps->arr[0] = x;
ps->size++;
}
//通过遍历数组来查找 返回下标
int SLFind(SL* ps, SLTDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
//找到了
return i;
}
}
//未找到
return -1;
}
//指定位置之前插入数据
//pos为指定位置的下标
void SLInsert(SL* ps, int pos, SLTDataType x)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
//判断空间是否足够
SLCheckCapacity(ps);
//pos及之后数据向后挪动一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
//指定位置之后插入数据
SLInsertAfter(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
//pos之前的数据向后挪动一位
for (int i = ps->size;i > pos+1;i--)
{
ps->arr[i] = ps->arr[i-1];
}
ps->arr[pos+1] = x;
ps->size++;
}
//删除指定位置的数据
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
//pos后面的数据向前挪动一位
for (int i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
三. 链表
链表是一种非连续、非顺序的存储结构,通过指针将一组零散的内存块串联起来。常见的链表有单链表、双向链表和循环链表。
1. 单向链表代码实现
- SList.h
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
//定义结构体
typedef struct SListNode
{
SLTDataType data;//节点的值
struct SListNode *next;//指向下一个节点的指针
}SLTNode;
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* phead, SLTDataType x);
//删除指定pos节点位置的数据
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除指定pos节点位置之后的数据
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SLisDesTroy(SLTNode** pphead);
//打印链表
void SLTPrint(SLTNode** pphead);
- SList.c
c
#include "SList.h"
//申请节点
SLTNode* STLBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("申请内存失败!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//打印链表
void SLTPrint(SLTNode* pphead)
{
SLTNode* ptail = pphead;
while (ptail)
{
printf("%d->", ptail->data);
ptail = ptail->next;
}
printf("NULL\n");
}
尾插:通过循环遍历到最后一个节点,把最后一个节点指向插入的节点;
头插:将插入的节点指向头节点,再把插入节点改为头节点;
c
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//*pphead 是指向第一个节点的指针
SLTNode* newnode = STLBuyNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = STLBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
尾删:循环遍历到最后一个节点,free释放掉节点;
头删:创建一个指向头节点下一节点的位置,再free释放掉节点
c
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
//prev->next = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode *ptail = *pphead;
*pphead = ptail->next;
free(ptail);
ptail = NULL;
}
c
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos && *pphead);
SLTNode* newnode = STLBuyNode(x);
if (pos== *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* phead, SLTDataType x)
{
assert(phead);
SLTNode* newnode = STLBuyNode(x);
newnode->next = phead->next;
phead->next = newnode;
}
//删除指定pos节点位置的数据
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos && *pphead);
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除指定pos节点位置之后的数据
void SLTEraseAfter(SLTNode* pos)
{
assert(pos&&pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
void SLisDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
2.双向链表代码实现
这里示例的是双向带头循环链表
双向链表我们在前面加上一个头节点head,next指向下一个节点的指针,prev指向上一个节点的指针。
- List.h
c
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义双向链表结构
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
struct ListNode* next; //指向下一个节点的指针
struct ListNode* prev; //指向前一个节点的指针
}LTNode;
//初始化
LTNode* LTInit();
//销毁---为了保持接口一致性
void LTDesTroy(LTNode* phead);
//在双向链表中,增删改查都不会改变头节点
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//判断是否为空
bool LTEmpty(LTNode* phead);
//打印
void LTPrint(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置的节点
void LTErase(LTNode* pos);
尾插:
phead = 头节点; phead->prev = 尾节点; newnode = 插入节点;
1. newnode->prev = phead->prev; 插入节点的prev指向尾节点
2. newnode->next = phead; 插入节点的next指向头节点
3. phead->prev->next = newnode;改变尾节点next指向插入节点
4. phead->prev = newnode; 改变头节点prev指向插入节点

c
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//phead phead->prev newnode
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
头插:
phead = 头节点; newnode = 插入节点 ; phead->next = 尾节点;
1. newnode->next = phead->next; 插入节点的next指向头节点指向的下一个节点
2. newnode->prev = phead; 插入节点的prev指向头节点
3. phead->next->prev = newnode;头节点指向下一个节点的prev指向插入节点
4. phead->next = newnode; 头节点的next指向插入节点

c
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//phead newnode phead->next
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
尾删:
1. LTNode* del = phead->prev;创建一个指针指向头节点的prev
2. del->prev->next = phead; 将del上一个的next指向头节点
3. phead->prev = del->prev; 将头节点的prev指向del的prev
4. 最后释放节点free(del)

c
//尾删
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
头删:
1. LTNode* del = phead->next;创建一个指针指向头节点的next
2. del->next->prev = phead; 将del下一个的prev指向头节点
3. phead->next = del->next; 将头节点的next指向del的next
4. 最后释放节点free(del)

c
//头删
void LTPopFront(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->next;
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
- List.c
c
#include"List.h"
//申请节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
//初始化
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
//销毁
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
//销毁头结点
free(phead);
phead = NULL;
}
//打印
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//判断是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//未找到
return NULL;
}
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
//pos newnode pos->next
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
//删除pos位置的节点
void LTErase(LTNode* pos)
{
assert(pos);
//pos->prev pos pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
四. 顺序表与链表的区别
特性 | 顺序表 | 单向链表 | 双向链表 |
---|---|---|---|
内存布局 | 连续的空间 | 节点通过指针链接,内存不连续 | 节点通过两个指针链接,内存不连续 |
访问方式 | 随机访问(通过下标直接访问)O(1) | 顺序访问(从头节点开始遍历)O(n) | 随机访问(可以正向和反向遍历)O(n) |
插入/删除 | 需要移动元素O(n) | 只需修改指针指向O(1) | 只需修改指针指向O(1) |
内存占用 | 仅存储数据 | 每个节点存储的数据和指向下一个节点的指针 | 每个节点存储数据和两个指针 |
适用场景 | 需要频繁的随机访问 | 需要频繁的插入和删除,且不需要随机访问 | 需要频繁插入和删除,且需要双向遍历的场景 |
总结
**本篇文章到这里就结束啦!**通过前面的介绍,相信大家对顺序表、单向链表和双向链表都有了更清晰的认识。顺序表凭借其高效的随机访问能力,在对数据快速定位有较高要求的场景中发挥着关键作用;单向链表以其灵活的插入和删除操作,在数据频繁变动且无需随机访问的情境下展现出优势;双向链表则在兼具单向链表灵活性的基础上,通过支持双向遍历,进一步拓展了应用范围。文章中如果大家发现有不对的地方可以直接指出,博主会积极改正。还希望大家多多谅解,最后感谢大家的点赞、收藏、评论和收藏。