线性表
线性表:具有n个相同类型数据元素的有限集合。常见的线性表:顺序表、链表、栈、队列、字符串等。
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
线性表特点:
- 集合中存在唯一的“第一个”数据元素。
- 集合中存在唯一的 “最后一个” 数据元素。
- 除最后一个元素之外,均有唯一的后继。
- 除第一个元素之外,均有唯一的前驱。
- 线性表中元素的个数有限。
- 线性表中元素的数据类型都相同,每个元素占用相同大小的存储空间。
顺序表
用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的两个元素在物理位置上也相邻。一般情况下采用数组存储。在数组上完成数据的增删查改。但是线性表中元素的次序是从1开始的,数组中元素的下标是从0开始的
静态顺序表(使用定长数组存储元素)
#define MaxSize 8 //定义线性表的最大长度
typedef int ElemType //ElemType为元素类型
typedef struct SeqList{
ElemType date[MaxSize]; //顺序表的元素
int size; //顺序表的当前长度
}SeqList;
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表。
动态顺序表(使用动态开辟的数组存储)
动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就会另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储数据空间的目的,不需要一次性划分所有空间。
typedef int SLDateType; //定义数据类型
typedef struct SeqList
{
SLDateType* a; //动态开辟数组
int size; //有效数据个数
int capacity; //容量空间的大小
}SeqList;
下面通过代码简单介绍顺序表的初始化、尾插尾删、头插头删、插入指定位置操作、删除指定位置操作以及查询操作,示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include <assert.h>
typedef int SLDateType; //定义数据类型
typedef struct SeqList
{
SLDateType* a;
int size;
int capacity;
}SeqList;
//初始化顺序表
void SeqListInit(SeqList* ps) {
assert(ps);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
//销毁顺序表
void SeqListDestroy(SeqList* ps) {
assert(ps);
if (ps) {
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
}
//打印顺序表
void SeqListPrint(SeqList* ps) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
printf("%d ", ps->a[i]);
}
printf("\n");
}
//尾插
//考虑扩容问题
void SeqListCheckCapacity(SeqList* ps) {
assert(ps);
if (ps->size == ps->capacity) {
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * newCapacity);
if (tmp == NULL) {
perror("realloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity = newCapacity;
}
}
void SeqListPushBack(SeqList* ps, SLDateType x) {
assert(ps);
SeqListCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
//头插
void SeqListPushFront(SeqList* ps, SLDateType x) {
assert(ps);
//考虑扩容
SeqListCheckCapacity(ps);
//从后向前挪动数据
int end = ps->size-1;
while (end >= 0) {
ps->a[end+1] = ps->a[end];
end--;
}
//实现头插
ps->a[0] = x;
ps->size++;
}
//头删
void SeqListPopFront(SeqList* ps) {
assert(ps);
//判空
assert(ps->size > 0);
//从后向前挪动
int begin = 1;
while (begin<ps->size)
{
ps->a[begin-1] = ps->a[begin];
begin++;
}
ps->size--;
}
//尾删
void SeqListPopBack(SeqList* ps) {
assert(ps);
assert(ps->size > 0);
ps->a[ps->size - 1] = NULL;
ps->size--;
}
// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x) {
assert(ps);
//循环查找
for (int i = 0; i < ps->size; i++) {
if (ps->a[i] == x) {
return i;
}
}
return -1;
}
// 顺序表在pos位置插入x
//插入考虑扩容问题
void SeqListInsert(SeqList* ps, int pos, SLDateType x) {
assert(ps);
assert(pos >= 0);
assert(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++;
}
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos) {
assert(ps);
assert(pos >= 0);
assert(pos < ps->size);
for (int i = pos + 1; i < ps->size; i++) {
ps->a[i-1] = ps->a[i];
}
ps->size--;
}
void menu() {
printf("************************\n");
printf("*****1、尾插数据********\n");
printf("*****2、尾删数据********\n");
printf("*****3、头插数据********\n");
printf("*****4、头删数据********\n");
printf("*****5、顺序表查找******\n");
printf("*****6、在pos位置插入x**\n");
printf("*****7、删除pos位置的值*\n");
printf("*****8、打印数据********\n");
printf("************************\n");
}
int main() {
int input = 0;
int val = 0;
int pos = 0;
SeqList s1;
SeqListInit(&s1);
menu();
do
{
printf("请输入你的选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入你要依次尾插的数,以-1结束:");
scanf("%d ", &val);
while(val != -1) {
SeqListPushBack(&s1, val);
scanf("%d ", &val);
}
break;
case 2:
SeqListPopBack(&s1);
break;
case 3:
printf("请输入你要头插的数:");
scanf("%d\n ", &val);
SeqListPushFront(&s1, val);
break;
case 4:
SeqListPopFront(&s1);
break;
case 5:
printf("请输入你要查找的数:");
scanf("%d\n ", &val);
SeqListFind(&s1, val);
break;
case 6:
printf("请输入你要插入的位置和数:");
scanf("%d %d\n ", &pos,&val);
SeqListInsert(&s1, pos, val);
break;
case 7:
printf("请输入你要删除的位置:");
scanf("%d\n ", &pos);
SeqListErase(&s1, pos);
break;
case 8:
printf("打印数据:");
SeqListPrint(&s1);
break;
default:
printf("您输入的数字有问题,应在1-7之间");
break;
}
} while (input);
SeqListDestroy(&s1);
链表
单链表
单链表是指通过一组任意的存储单元来存储线性表中的数据元素。
单链表结点类型表示为:
typedef int SLTDateType; //定义数据类型
typedef struct SListNode
{
SLTDateType data; //数据域
struct SListNode* next; //指针域
}SListNode;
单链表可以解决顺序表需要大量存储单元的缺点,但单链表附加指针域,也存在浪费存储空间的缺点。单链表的存储空间离散地分布在存储空间中,单链表是非随机存取地存储结构。在使用单链表查找数据元素时,不能直接找到某个特定地结点,需要从表头开始遍历,依次比较查找。
下面通过代码简单介绍单链表的初始化、尾插尾删、头插头删、插入指定位置操作、删除指定位置操作以及查询操作,示例代码如下:
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x) {
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
if (newNode == NULL) {
perror("malloc fail");
exit(-1);
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
// 单链表打印
void SListPrint(SListNode* plist) {
SListNode* cur = plist;
while (cur!=NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x) {
SListNode* newNode = BuySListNode(x);
//如果链表为空
if (*pplist == NULL) {
*pplist = newNode;
}
else {
//不为空,遍历找到最后一个节点
SListNode* cur = *pplist;
while (cur->next) {
cur = cur->next;
}
cur->next = newNode;
}
}
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x) {
SListNode* newNode = BuySListNode(x);
newNode->next = *pplist;
*pplist = newNode;
}
// 单链表的尾删
void SListPopBack(SListNode** pplist) {
//判空
assert(*pplist);
//只有一个链表
if ((*pplist)->next==NULL) {
free(*pplist);
*pplist= NULL;
}
else {
SListNode* cur = *pplist;
while (cur->next->next)
{
cur = cur->next;
}
free(cur->next);
cur->next = NULL;
}
}
// 单链表头删
void SListPopFront(SListNode** pplist) {
assert(*pplist);
if ((*pplist)->next == NULL) {
free(*pplist);
*pplist = NULL;
}
else {
//记住头节点下一节点
SListNode* cur = (*pplist)->next;
free(*pplist);
*pplist = cur;
}
}
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x) {
SListNode* cur = plist;
while (cur) {
if (cur->data == x) {
return cur;
}
cur = cur->next;
}
return NULL;
}
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x) {
assert(pos);
SListNode* newNode = BuySListNode(x);
newNode->next = pos->next;
pos->next = newNode;
}
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos) {
assert(pos);
if (pos->next == NULL) {
return;
}
else
{
SListNode* newNode = pos->next;
pos->next = newNode->next;
free(newNode);
}
}
// 单链表的销毁
void SListDestroy(SListNode** pplist) {
SListNode* cur =* pplist;
while (cur) {
SListNode* next = cur->next;
free(cur);
cur = next;
}
*pplist = NULL;
}
void menu() {
printf("**************************\n");
printf("*****1、尾插数据**********\n");
printf("*****2、尾删数据**********\n");
printf("*****3、头插数据**********\n");
printf("*****4、头删数据**********\n");
printf("*****5、单链表查找********\n");
printf("*****6、在pos位置后插入x**\n");
printf("*****7、删除pos位置后的值*\n");
printf("*****8、打印数据**********\n");
printf("**************************\n");
}
int main() {
int input = 0;
int val = 0;
int pos = 0;
SListNode* plist = NULL;
menu();
do
{
printf("请输入你的选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入你要依次尾插的数,以-1结束:");
scanf("%d ", &val);
while(val != -1) {
SListPushBack(&plist, val);
scanf("%d ", &val);
}
break;
case 2:
SListPopBack(&plist);
break;
case 3:
printf("请输入你要头插的数:");
scanf("%d\n ", &val);
SListPushFront(&plist, val);
break;
case 4:
SListPopFront(&plist);
break;
case 5:
printf("请输入你要查找的数:");
scanf("%d\n ", &val);
SListFind(plist, val);
break;
case 6:
printf("请输入你要插入位置和数:");
scanf("%d %d\n ",&pos,&val);
SListNode* p = SListFind(plist, pos);
SListInsertAfter(p, val);
break;
case 7:
printf("请输入你要删除的位置:");
scanf("%d\n ", &pos);
p = SListFind(plist,val);
SListEraseAfter(p);
break;
case 8:
printf("打印数据:");
SListPrint(plist);
break;
default:
printf("您输入的数字有问题,应在1-7之间");
break;
}
} while (input);
SListDestroy(&plist);
return 0;
}
双向链表
单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点地前驱结点(插入、删除操作)时,只能从头开始遍历,访问后继结点时地时间复杂度为 O(1),访问前驱结点的时间复杂度为 O(n)。
为了克服单链表上的缺陷,引入了双链表,双链表结点中有两个指针prev和next,分别为指向其前驱结点和后继结点。
双链表中结点类型的描述如下:
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data; //数据域
struct ListNode* _next; //后继结点
struct ListNode* _prev; //前驱结点
}ListNode;
下面通过代码简单介绍双向链表的初始化、尾插尾删、头插头删、插入指定位置操作、删除指定位置操作以及查询操作,示例代码如下:
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
//创建新结点
ListNode* BuyListNode(LTDataType x) {
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
if (newNode == NULL) {
perror("malloc fail");
exit(-1);
}
newNode->_data = x;
newNode->_next = NULL;
newNode->_prev = NULL;
return newNode;
}
//链表初始化
ListNode* ListInit() {
ListNode* pHead = BuyListNode(-1);
pHead->_next = pHead;
pHead->_prev = pHead;
return pHead;
}
// 双向链表打印
void ListPrint(ListNode* pHead) {
ListNode* cur = pHead->_next;
while (cur!=pHead)
{
printf("%d ", cur->_data);
cur = cur->_next;
}
printf("\n");
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x) {
ListNode* newNode = BuyListNode(x);
//找到最后一个节点
ListNode* tail = pHead->_prev;
tail->_next = newNode;
newNode->_prev = tail;
newNode->_next = pHead;
pHead->_prev = newNode;
}
// 双向链表尾删
void ListPopBack(ListNode* pHead) {
assert(pHead);
assert(pHead->_next != NULL);
ListNode* tail = pHead->_prev;
ListNode* prev = tail->_prev;
prev->_next = pHead;
pHead->_prev = prev;
free(tail);
}
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x) {
ListNode* newNode = BuyListNode(x);
ListNode* next = pHead->_next;
pHead->_next = newNode;
newNode->_prev = pHead;
newNode->_next = next;
next->_prev = newNode;
}
// 双向链表头删
void ListPopFront(ListNode* pHead) {
assert(pHead);
assert(pHead->_next!=pHead);
ListNode* first = pHead->_next;
ListNode* second = first->_next;
free(first);
pHead->_next = second;
second->_prev = pHead;
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x) {
ListNode* cur = pHead->_next;
while (cur != pHead) {
if (cur->_data == x) {
return cur;
}
cur = cur->_next;
}
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x) {
ListNode* newNode = BuyListNode(x);
ListNode* prev = pos->_prev;
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = pos;
pos->_prev = newNode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos) {
ListNode* next = pos->_next;
ListNode* prev = pos->_prev;
free(pos);
prev->_next = next;
next->_prev = prev;
}
// 双向链表销毁
void ListDestory(ListNode* pHead) {
ListNode* cur = pHead->_next;
while (cur != pHead) {
ListNode* next = cur->_next;
free(cur);
cur = next;
}
free(pHead);
}
顺序表和链表区别
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |