线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就是说连续的一条直线。
但是在物理结构上不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一定是连续存储数据的,不能跳跃。
顺序表一般可以分为:
静态顺序表:使用定长数组存储元素。
以下源代码在
SeqList.h
中#pragma once //确保头文件只被包含一次,避免被重复包含 #define N 10 typedef int SLDataType; //静态顺序表 struct SeList { SLDataType a[N];//定长数组 int size;//存储数据的个数 };
静态的顺序表是有缺陷的,静态顺序表在创建时需要指定一个固定的大小,这个大小通常在编译时就确定了。这就意味着,一旦分配的空间用完,无法动态调整表的大小。如果实际数据量超过了静态顺序表的大小,就可能导致溢出或者需要重新定义一个更大的静态顺序表,这会浪费内存或者导致程序运行错误。
动态顺序表:使用动态开辟的数组存储。
//动态顺序表 typedef int SLDataType; struct SeList { SLDataType* a; //指向动态开辟的数组 int size; //有效数据的个数 int capacity; //存储空间的大小 //空间不够则增容 };
动态顺序表可以动态分配空间的大小,可以扩容。
一般是扩容2倍比较合适,其他倍数也可行,只不过扩容扩多了会存在空间浪费,扩少了就会导致频繁扩容,效率损失,所以扩容2倍比较合适。
静态顺序表只适用于确定知道需要存多少数据的场景。
静态顺序表的定长数组导致 N 定大了,空间开多了浪费,开少了不够用。
所以现实中基本都是使用动态顺序表,根据需要动态地分配空间大小。
数据结构是在内存中管理数据:增删查改。
有一个原则:要控制顺序表,实现增删查改的访问等操作,不要直接访问结构体,要通过对应的函数。
//动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a; //指向动态开辟的数组
int size; //存储数据的个数
int capacity; //存储空间的大小
//空间不够则增容
}SL;//实现接口更方便
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void TestSeqList1()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushBack(&sl, 5);
SLPushBack(&sl, 6);
SLPrint(&sl);
SLPushFront(&sl, 10);//头插有代价,尽量少用,时间复杂度为 O(N)
SLPushFront(&sl, 20);
SLPushFront(&sl, 30);
SLPrint(&sl);
SLDestory(&sl);
}
void TestSeqList2()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPrint(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPrint(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPrint(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPrint(&sl);
SLPushBack(&sl, 10);
SLPushBack(&sl, 20);
SLPushBack(&sl, 30);
SLDestory(&sl);
}
void TestSeqList3()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPrint(&sl);
SLPopFront(&sl);
SLPopFront(&sl);
SLPrint(&sl);
}
TestSeqList4()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushBack(&sl, 5);
SLPrint(&sl);
SLInsert(&sl, 0, 30);
SLPrint(&sl);
SLErase(&sl, 0);
SLPrint(&sl);
//int x = 0;
//scanf("%d", &x);
//int pos = SLFind(&sl, x);
//if (pos != -1)
//{
// SLInsert(&sl, pos, 10 * x);
//}
//SLPrint(&sl);
}
int main()
{
//TestSeqList1();
//TestSeqList2();
//TestSeqList3();
TestSeqList4();
return 0;
}
SeqList.h
#pragma once
#include
#include
#include
//动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a; //指向动态开辟的数组
int size; //存储数据的个数
int capacity; //存储空间的大小
//空间不够则增容
}SL;
//顺序表初始化
void SLInit(SL* psl);
//顺序表销毁
void SLDestory(SL* psl);
//增删查改
//顺序表打印
void SLPrint(const SL* psl);
//尾插
void SLPushBack(SL* psl, SLDataType x);
//头插
void SLPushFront(SL* psl, SLDataType x);
//检查容量
void SLCheckCapacity(SL* psl);
//头删
void SLPopFront(SL* psl);
//尾删
void SLPopBack(SL* psl);
//搜索,找到返回下标,没找到返回-1
int SLFind(SL* psl, SLDataType x);
//在 pos 位置插入 x(可以代替头插尾插)
void SLInsert(SL* psl, size_t pos, SLDataType x);
//删除 pos 位置的值
void SLErase(SL* psl, size_t pos);
//修改 pos 位置的值
void SLModify(SL* psl, size_t pos, SLDataType x);
SeqList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"
void SLPrint(const SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; ++i)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
void SLInit(SL* psl)
{
assert(psl);
psl->a = NULL;
psl->capacity = psl->size = 0;
}
void SLDestory(SL* psl)
{
assert(psl);
free(psl->a);
psl->a = NULL;
psl->capacity = psl->size = 0;
}
void SLPushBack(SL* psl, SLDataType x)
{
assert(psl);
SLCheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;
}
void SLPushFront(SL* psl, SLDataType x)
{
assert(psl);
SLCheckCapacity(psl);
//挪动数据->从后往前挪
int end = psl->size - 1;
while (end >= 0)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[0] = x;
psl->size++;
}
void SLCheckCapacity(SL* psl)
{
//扩容是以空间换时间
//缩容是以时间换空间
//坚决不缩容
//检查容量
if (psl->size == psl->capacity)
{
int newCapcity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = realloc(psl->a, newCapcity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail");
//return;
exit(-1);
}
psl->a = tmp;
psl->capacity = newCapcity;
}
}
void SLPopBack(SL* psl)
{
assert(psl);
//温柔的检查
if (psl->size == 0)
{
return;
}
暴力的检查
//assert(psl->size);
psl->size--;
}
void SLPopFront(SL* psl)
{
assert(psl);
SLCheckCapacity(psl);
int begin = 0;
while (begin <= psl->size - 2)
{
psl->a[begin] = psl->a[begin + 1];
begin++;
}
if (psl->size == 0)
{
return;
}
psl->size--;
}
int SLFind(SL* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
void SLInsert(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos <= psl->size);
SLCheckCapacity(psl);
//挪动数据
size_t end = psl->size;
while (end > pos)
{
psl->a[end] = psl->a[end - 1];
end--;
}
if (pos == 0)
{
psl->a[pos + 1] = psl->a[pos];
}
psl->a[pos] = x;
psl->size++;
}
void SLErase(SL* psl, size_t pos)
{
assert(psl);
size_t cur = pos + 1;
while (cur < psl->size)
{
psl->a[cur - 1] = psl->a[cur];
cur++;
}
psl->size--;
}
void SLModify(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos < psl->size);
psl->a[pos] = x;
}
1.原地移除数组中所有的元素val,要求时间复杂度为O(N),空间复杂度为O(1)。
int removeElement(int* nums, int numsSize, int val) {
int src = 0, dst = 0;
while(src < numsSize)
{
if(nums[src] != val)
{
nums[dst] = nums[src];
++src;
++dst;
}
else
{
++src;
}
}
return dst;
}
2.删除排序数组中的重复项。
int removeDuplicates(int* nums, int numsSize) {
int src = 0, dst = 0;
while(src < numsSize)
{
if(nums[src] == nums[dst])
{
++src;
}
else
{
nums[++dst] = nums[src++];
}
}
return dst + 1;
}
3.合并两个有序数组。
若 nums1 的数先结束比较,那么可以不做任何操作,但是如果 nums2 先结束,就需要把 nums2 中剩余的数拷贝到 nums1 中
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int end1 = m - 1, end2 = n - 1;
int i = m + n - 1;
while(end1 >= 0 && end2 >= 0)
{
if(nums1[end1] > nums2[end2])
{
nums1[i] = nums1[end1];
--i;
--end1;
}
else
{
nums1[i] = nums2[end2];
--i;
--end2;
}
}
//end2 结束, nums2的数组都拷贝过去了,不用处理
//end1 结束, nums1的数组都拷贝过去了,需要再把nums2剩下的数据拷贝过去
while(end2 >= 0)
{
nums1[i] = nums2[end2];
--i;
--end2;
}
}
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:该如何解决以上问题?
下篇文章我们来使用链表的结构。
概念:链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
现实中 数据结构中
实际中链表的结构非常多样,以下情况组合起来就有 8 种链表结构:
- 单向或双向:
- 带头或不带头:
- 循环或非循环:
虽然有这么多的链表的结构,但是我们实际中最常用的还是这两种结构:
- 无头单项非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单。
无头+单向+非循环链表增删查改实现
Slist.h
#pragma once
#include
#include
#include
typedef int SLTDataType;
typedef struct SlistNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode,*PSLTNode;
//以下三种形式等价
//SLTNode*
//PSLTNode
//struct SlistNode
//动态申请一个结点
SLTNode* BuySLTNode(SLTDataType x);
//打印链表
void SListPrint(SLTNode* phead);
//void SListPrint(PSLTNode phead);
//销毁链表
//不及时销毁链表是一种内存泄露
void SListDestory(SLTNode** pphead);
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//尾删
void SListPopBack(SLTNode** pphead);
//头删
void SListPopFront(SLTNode** pphead);
//查找
//可以充当修改
SLTNode* SlistFind(SLTNode* phead, SLTDataType x);
//在pos之前插入
void SlistInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos之后插入
void SlistInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos位置
void SlistErase(SLTNode** pphead, SLTNode* pos);
//删除pos后面位置
void SlistEraseAfter(SLTNode* pos, SLTDataType x);
Slist.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Slist.h"
void SListPrint(SLTNode* phead)
{
//不能断言,因为phead为空是正常情况,表示空链表
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
//1. 链表为空
//2. 链表非空
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找尾
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
//尾插改变的是结构体成员,所以不用二级指针(结构体指针的指针),用结构体指针
}
}
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
//1.多个节点
//2.一个节点
if (*pphead == NULL)
{
return;
}
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
prev->next = NULL;
free(tail);
tail = NULL;
}
}
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
//温柔的检查
if (*pphead == NULL)
{
return;
}
暴力检查
//assert(*pphead != NULL);
SLTNode* del = *pphead;
*pphead = (*pphead)->next;
free(del);
del = NULL;
}
void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
SLTNode* SlistFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SlistInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
//暴力检查,pos不在链表中,prev为空还没有找到pos说明pos传错了
assert(prev);
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
void SlistInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
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;
//检查pos不是链表中节点,参数传错了
assert(prev);
}
prev->next = pos->next;
free(pos);
}
}
void SlistInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Slist.h"
void TestSlist1()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPrint(plist);
}
void TestSlist2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
//SListPopBack(&plist);
//SListPopBack(&plist);
//SListPopBack(&plist);
//SListPopBack(&plist);
//SListPopBack(&plist);
//SListDestory(&plist);
SLTNode* pos = SlistFind(plist, 3);
if (pos)
{
//修改
//SlistInsert(&plist, pos, 20);
SlistErase(&plist, pos);
printf("找到了\n");
}
else
{
printf("没找到\n");
}
SListPrint(plist);
}
int main()
{
//TestSlist1();
TestSlist2();
return 0;
}
单链表只适合头插头删,时间复杂度 O(1)
任意位置高效插入删除要交给之后文章讲解的双向链表。
要求删除 pos 位置,时间复杂度是 O(1)
要求在pos位置之前插入,要求 O(1)
删除链表中等于给定值 val 的所有结点。OJ链接
反转一个单链表。OJ链接
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ链接
输入一个链表,输出该链表中倒数第k个结点。OJ链接
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。OJ链接
编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大小或等于x的结点之前。OJ链接
链表的回文结构。OJ链表
输入两个链表,找出它们的第一个公共结点。OJ链表
给定一个链表,判断链表中是否有环。OJ链表
【思路】
快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。
【扩展问题】
- 为什么快指针每次走两步,慢指针走一步可以?
假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。
此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。
- 快指针一次走3步,走4步,…n步行吗?
给定一个链表,返回链表开始入环的第一个结点。如果链表无环,则返回 NULL。OJ链接
结论
让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置 开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。
证明
- 给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点或空结点。
要求返回这个链表的深度拷贝。OJ链接
- 其他。ps:链表的题当前因为难度及知识面等等原因还不适合当前学习,下面有OJ链接。
Leetcode OJ链接 + 牛客 OJ链接
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持 O(1) | 不支持 O(N) |
任意位置插入或删除元素 | 可能需要搬移元素,效率低 O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
备注:缓存利用率参考存储体系结构以及局部原理性。
顺序表优点:
- 尾插尾删效率很高。
- 随机访问。(用下标访问)’
- 相比链表结构:cpu高速缓存命中率更高。
顺序表缺点:
- 头部和中部插入删除效率低。 —O(N)
- 扩容。 性能消耗+空间浪费
链表优点:
- 任意位置插入删除效率很高。 O(1)
- 按需申请释放。
链表缺点:
- 不支持随机访问
problemset/) + 牛客 OJ链接
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持 O(1) | 不支持 O(N) |
任意位置插入或删除元素 | 可能需要搬移元素,效率低 O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
备注:缓存利用率参考存储体系结构以及局部原理性。
顺序表优点:
- 尾插尾删效率很高。
- 随机访问。(用下标访问)’
- 相比链表结构:cpu高速缓存命中率更高。
顺序表缺点:
- 头部和中部插入删除效率低。 —O(N)
- 扩容。 性能消耗+空间浪费
链表优点:
- 任意位置插入删除效率很高。 O(1)
- 按需申请释放。
链表缺点:
- 不支持随机访问
[外链图片转存中…(img-Jzb5Rq09-1705152040679)]
与程序员相关CPU缓存知识