文章目录
一、线性表
是什么线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列; 线性表是一种在实际中广泛使 用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串...
线性表的结构
线性表在逻辑上是线性结构,也就说是连续的一条直线;但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
本篇文章,我们先讲解数组形式存储结构存储的线性表!
二、顺序表
1、什么是顺序表
顺序表是用一段物理地址连续 的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删查改。
简单来说,顺序表就是数组,只是要求数组里面的元素必须连续存储而已。
2、顺序表的分类
顺序一般分为两类:静态顺序表和动态顺序表。
静态顺序表:采用定长数组来存储元素。(一般不考虑)
c
#define MAX 1000 //数组的最大长度 typedef int SLDtataType; //重命名数据类型 typedef struct SeqList { SLDtataType data[MAX]; //使用定长数组来存储数据 size_t size; //有效数据的个数 }SL;
动态顺序表:使用动态开辟的数据来存储元素。(重点 有三个元素组成)
cpp
typedef int SLDataType; //将数据类型重命名为SLDataType
typedef struct SeqList {
SLDataType* data; //对应数据类型的指针,用来指向动态开辟的空间
size_t size; //记录当前有效数据的个数
size_t capacity; //记录当前容量,不够就增容
}SL;
两种顺序表的对比 :相较于动态顺序表,静态顺序表存在很大的缺陷,那就是空间问题:当我们数据量很大时给定的空间可能不够用,但我们数据量比较小时,给定的空间又可能过大,造成空间浪费,即静态顺序表只适用于确定知道需要存多少数据的场景;所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,而静态顺序表很少使用。下面我们用C语言来模拟实现一个动态的顺序表。
三、动态顺序表的实现
1、结构的定义
c
#define DEF_SIZE 5 //初始容量
#define CRE_SIZE 2 //一次扩容的倍数
typedef int SLDataType; //将数据类型重命名为SLDataType
typedef struct SeqList {
SLDataType* data; //对应数据类型的指针,用来指向动态开辟的空间
size_t size; //记录当前有效数据的个数
size_t capacity; //记录当前容量,不够就增容
}SL;
如上:我们将要管理的数据类型重命名为SLDateType,这样以后当我们要用此顺序表管理其他数据类型时,我们就只需要改动这一个地方。
其次,相较于静态顺序表,我们的结构体多了一个参数 -- capacity,我们用它来记录顺序表当前的容量,当当前的有效数据个数size与它相等时,我们就进行扩容;由于数据个数和顺序表的容量都不可能小于0,所以我们将其定义为size_t的。
最后就是关于初始容量和每次扩增倍数的问题,这里把初始容量设定为5,然后把扩增倍数设定为2,即我们的顺序表每次扩容两倍。
2、顺序表的初始化
在初始化函数中,我们把size和capacity都置为相应大小,并且为data指针动态开辟一块空间,用于存储数据。
c
void SLInit(SL* psl)
{
assert(psl);
psl->a = NULL;
psl->capacity = psl->size = 0;
}
3、检查容量
在检查容量的函数中,当我们结构体中的size和capacity相等时,我们就扩容,在扩容时我们要注意不要直接用data指针来接收realloc函数的返回值,避免扩容失败导致data指针找不到之前管理的空间,从而造成内存泄漏。
c
void SLCheckCapacity(SL* psl)
{
// 检查容量
if (psl->size == psl->capacity)
{
int newCapcity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(psl->a, newCapcity*sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
//exit(-1);
}
psl->a = tmp;
psl->capacity = newCapcity;
}
}
这里realloc有一个知识点:
我们realloc 后面写的是个数*sizeof(SLDatatype) 而不是单纯的个数 因为我们开辟的是空间大小
4、在头部插入数据
在头部插入数据时,我们需要先将顺序表中的数据整体向后挪动一位,然后在顺序表的开头插入;在插入完成后记得要让size++。
c
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++;
}
5、在尾部插入数据
在尾部插入数据很简单,直接插入就行。
c
void SLPushBack(SL* psl, SLDataType x)
{
assert(psl);
SLCheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;
}
6、在指定位置插入数据
在此函数中,我们需要先将pos及其之后的元素整体向后挪动一位,然后再在pos处插入数据。
c
//void SLInsert(SL* psl, int pos, SLDataType x)
void SLInsert(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos <= psl->size);
SLCheckCapacity(psl);
// 挪动数据
/*int end = psl->size - 1;
while (end >= (int)pos)
{
psl->a[end + 1] = psl->a[end];
--end;
}*/
size_t end = psl->size;
while (end > pos)
{
psl->a[end] = psl->a[end-1];
--end;
}
psl->a[pos] = x;
++psl->size;
}
由于尾插和头插也可以通过调用 SeqListInsret 函数实现,所以我们可以对头插和尾插函数进行改造,以此来简化代码:
在头部插入数据
c
void SLPushFront(SL* psl, SLDataType x)
{
SLInsert(psl, 0, x);
}
在尾部插入数据
c
void SLPushBack(SL* psl, SLDataType x)
{
SLInsert(psl, psl->size, x);
}
7、在尾部删除数据
删除尾部的数据很简单,我们只需要将size--即可,并不需要对其进行改动,因为我们下一次插入数据时会直接将原来空间中的数据覆盖掉。
c
void SLPopBack(SL* psl)
{
//assert(psl);
温柔的检查
///*if (psl->size == 0)
//{
//return;
//}*/
暴力的检查
//assert(psl->size > 0);
//psl->size--;
SLErase(psl, psl->size - 1);
}
8、在头部删除数据
在头部删除数据,我们只需要将顺序表中的数据整体向前挪动一位,然后将size--即可。
c
void SLPopFront(SL* psl)
{
//assert(psl);
//assert(psl->size > 0);
///*int begin = 0;
//while (begin < psl->size-1)
//{
// psl->a[begin] = psl->a[begin + 1];
// ++begin;
//}*/
//int begin = 1;
//while (begin < psl->size)
//{
// psl->a[begin-1] = psl->a[begin];
// ++begin;
//}
//--psl->size;
SLErase(psl, 0);
}
9、删除指定位置的数据
删除指定位置数据,我们需要将pos后面的数据整体向前挪动一位,然后让size--。
c
void SLErase(SL* psl, size_t pos)
{
assert(psl);
assert(pos < psl->size);
size_t begin = pos;
while (begin < psl->size - 1)
{
psl->a[begin] = psl->a[begin + 1];
++begin;
}
psl->size--;
}
和上面的插入数据一样,我们也可以通过调用 SeqListErase 函数来实现数据的头删和尾删。
**面试题:删除数据是否要缩容?**
我们知道,插入数据空间不够时我们要增容,那么删除数据达到一定的数量后我们是否要缩容呢?答案是不用缩容。原因如下:
第一:我们缩容之后插入数据又需要重新增容,而增容是有代价的,会降低程序的效率。我们知道 realloc 函数扩容分为两种情况,一种是原地扩,即当原来的空间后面有足够的空闲空间时,操作系统会直接将那一块空间交由我们使用,这种情况对效率影响不大;另一种是异地扩,即当原来空间后面没有足够的的空间开辟时,操作系统会在另外空间足够的地方为我们开辟一块新的空间,这时操作系统需要先将我们原来空间中的数据拷贝到新空间中,再将原来的空间释放掉,这种情况对效率的影响就比较大了。
第二:缩容也是有代价的。其实缩容和扩容的过程是一样的,都分为原地和异地,会对程序效率造成影响。
第三:顺序表申请的是一块连续的空间,而free函数数并不能释放连续空间的一部分,只能全部一起释放,所以这里即使想释放也是做不到的。
所以综合前面三个因素考虑,顺序表删除数据不会缩容;这是我们典型的**以空间换时间**的做法。
10、查找数据
当我们找到该元素时,我们返回元素的下标;当该元素不存在时,我们返回一个无意义的值。(如-1)
c
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;
}
11、修改指定位置的数据
c
void SLModify(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos < psl->size);
psl->a[pos] = x;
}
12、打印顺序表中的数据
c
void SLPrint(const SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; ++i)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
13、顺序表的销毁
在销毁顺序表的时候我们一定要记得将前面动态开辟的空间释放掉,防止内存泄漏。
c
void SLDestory(SL* psl)
{
assert(psl);
/*if (psl->a)
{*/
free(psl->a);
psl->a = NULL;
psl->capacity = psl->size = 0;
//}
}
四、完整代码
1、SeqLIst.h
c
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//#ifndef __SEQLIST_H__
//#define __SEQLIST_H__
...
//#endif
// Shunxubiao -- 二狗 铁柱 招弟
// 静态的顺序表 -- 不知道要存储多少数据?
//#define N 10000
//typedef int SLDataType;
//struct SeqList
//{
// SLDataType a[N];
// int size; // 存储数据的个数
//};
// 动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size; // 存储数据的个数
int capacity; // 存储空间的大小
}SL;
//void SeqListInit(SL 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 SLPopBack(SL* psl);
void SLPopFront(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);
void SLModify(SL* psl, size_t pos, SLDataType x);
2、SeqList.c
c
#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);
/*if (psl->a)
{*/
free(psl->a);
psl->a = NULL;
psl->capacity = psl->size = 0;
//}
}
void SLCheckCapacity(SL* psl)
{
// 检查容量
if (psl->size == psl->capacity)
{
int newCapcity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(psl->a, newCapcity*sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
//exit(-1);
}
psl->a = tmp;
psl->capacity = newCapcity;
}
}
// https://cplusplus.com/reference/
void SLPushBack(SL* psl, SLDataType x)
{
/*assert(psl);
SLCheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;*/
SLInsert(psl, psl->size, x);
}
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++;
SLInsert(psl, 0, x);
}
void SLPopBack(SL* psl)
{
//assert(psl);
温柔的检查
///*if (psl->size == 0)
//{
//return;
//}*/
暴力的检查
//assert(psl->size > 0);
//psl->size--;
SLErase(psl, psl->size - 1);
}
void SLPopFront(SL* psl)
{
//assert(psl);
//assert(psl->size > 0);
///*int begin = 0;
//while (begin < psl->size-1)
//{
// psl->a[begin] = psl->a[begin + 1];
// ++begin;
//}*/
//int begin = 1;
//while (begin < psl->size)
//{
// psl->a[begin-1] = psl->a[begin];
// ++begin;
//}
//--psl->size;
SLErase(psl, 0);
}
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, int pos, SLDataType x)
void SLInsert(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos <= psl->size);
SLCheckCapacity(psl);
// 挪动数据
/*int end = psl->size - 1;
while (end >= (int)pos)
{
psl->a[end + 1] = psl->a[end];
--end;
}*/
size_t end = psl->size;
while (end > pos)
{
psl->a[end] = psl->a[end-1];
--end;
}
psl->a[pos] = x;
++psl->size;
}
// 顺序表删除pos位置的值
void SLErase(SL* psl, size_t pos)
{
assert(psl);
assert(pos < psl->size);
size_t begin = pos;
while (begin < psl->size - 1)
{
psl->a[begin] = psl->a[begin + 1];
++begin;
}
psl->size--;
}
void SLModify(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos < psl->size);
psl->a[pos] = x;
}
3、test.c
c
//#include <stdio.h>
//#include <stdlib.h>
//
//void f1(int n)
//{
// int a = 0;
// int* p = (int*)malloc(4 * n);
//
// printf("%p,%p\n", &a, &p);
// if(p == NULL)
// {
// perror("malloc fail");
// return;
// }
//
// free(p);
//}
//
2N+2 个数
N+1 个数
//int main()
//{
// f1(10);
// f1(10);
//
// return 0;
//}
#include "SeqList.h"
void TestSeqList1()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPushBack(&s, 6);
SLPrint(&s);
SLPushFront(&s, 10);
SLPushFront(&s, 20);
SLPushFront(&s, 30);
SLPrint(&s);
SLDestory(&s);
}
void TestSeqList2()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPrint(&s);
SLPopBack(&s);
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPopBack(&s);
SLPrint(&s);
//SLPopBack(&s);
//SLPrint(&s);
SLPushBack(&s, 10);
SLPushBack(&s, 20);
SLPushBack(&s, 30);
SLPushBack(&s, 40);
SLPrint(&s);
SLDestory(&s);
}
void TestSeqList3()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPrint(&s);
SLPopFront(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPopFront(&s);
SLPrint(&s);
SLPushBack(&s, 10);
SLPushBack(&s, 20);
SLPushBack(&s, 30);
SLPushBack(&s, 40);
SLPrint(&s);
int* p1 = (int*)malloc(sizeof(int)* 10);
assert(p1);
printf("p1:%p\n", p1);
int* p2 = (int*)realloc(p1, sizeof(int)* 5);
assert(p2);
printf("p2:%p\n", p2);
}
void TestSeqList4()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPrint(&s);
//SLInsert(&s, 2, 30);
SLInsert(&s, 0, 30);
SLPrint(&s);
int x = 0;
scanf("%d", &x);
int pos = SLFind(&s, x);
if (pos != -1)
{
SLInsert(&s, pos, x * 100);
}
SLPrint(&s);
}
void TestSeqList5()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPrint(&s);
SLErase(&s, 0);
SLPrint(&s);
int x = 0;
scanf("%d", &x);
int pos = SLFind(&s, x);
if (pos != -1)
{
SLErase(&s, pos);
}
SLPrint(&s);
}
void memu()
{
printf("****************************************\n");
printf("1、尾插数据 2、头插数据\n");
printf("7、打印数据 -1、退出\n");
printf("****************************************\n");
}
int main()
{
SL s;
SLInit(&s);
int option = 0;
int x = 0;
do
{
memu();
printf("请输入你的操作:>");
scanf("%d", &option);
switch (option)
{
case 1:
printf("请连续输入你要插入的数据,以-1结束\n");
scanf("%d", &x);
while (x != -1)
{
SLPushBack(&s, x);
scanf("%d", &x);
}
break;
case 2:
break;
case 3:
break;
case 7:
SLPrint(&s);
break;
default:
break;
}
} while (option != -1);
SLDestory(&s);
return 0;
}
五、顺序表的缺陷
顺序表存在如下缺陷:
- 在头部/中间的插入与删除需要挪动数据,时间复杂度为O(N),效率低;
- 增容需要申请新空间,可能会拷贝数据,释放旧空间,会有不小的消耗;
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,如果我们再继续插入了5个数据,后面没有数据插入了,那么会浪费95个数据空间;
针对顺序表存在的这些缺陷,我们设计出了链表。
六、顺序表力扣OJ题
1、移除元素
题目链接
题目描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
思路分析
思路1
循环遍历,挪动覆盖数据:遍历这个数组,每当遇见等于val的元素时,就将数组后面的元素整体向前挪动一位。
时间复杂度:O(N^2) 空间复杂度:O(1)。
思路2
遍历数组,挪动元素到新空间:开辟一个新的数组,然后遍历原来的数组,将不等于val的元素放入新开辟的数组中,将等于val的元素丢弃;最后让新开辟的数组覆盖掉原数组。
时间复杂度:O(N) 空间复杂度:O(N) 因为开辟了一个新空间。
思路3
遍历数组,挪动覆盖数据(双指针解法):定义两个指针src 和 dst,最开始都指向数组第一个元素,然后开始遍历数组;如果arr[src] != val,那么让arr[dst] = arr[src],然后 src++,dst++;如果 arr[src]==val,则 src++,dst不动;这样一直往后遍历,知道把数组所有元素都遍历完。
思路三代码实现
c
int removeElement(int* nums, int numsSize, int val) {
int src=0,dst=0;
while(src < numsSize)
{
if(nums[src]==val)
{
++src;
}
else
{
nums[dst]=nums[src];
++dst;
++src;
}
}
return dst;
}
2、删除有序数组中的重复项
题目链接
26. 删除有序数组中的重复项 - 力扣(LeetCode)
题目描述
给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。
将最终结果插入 nums 的前 k 个位置后返回 k 。不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
思路分析
这道题的解题思路和上面的移除元素十分相似,都是使用双指针;让 src 和 dst 都指向数组第一个元素,然后判断 arr[src] 和 arr[dst] 是否相等,如果相等,说明是重复元素,则让 src++,dst 不动;如果不相等,就让dst++,然后将 arr[src] 赋给 arr[dst],再让 src++;然后一直往后遍历数组,直到将数组遍历完。
时间复杂度:O(N) 空间复杂度:O(1)
代码实现
c
int removeDuplicates(int* nums, int numsSize) {
int src = 0;
int dst = 0;
while (src < numsSize) {
if (nums[src] != nums[dst]) {
nums[++dst] = nums[src++]; // 注意:dst 是前置++
} else {
src++;
}
}
return dst + 1; // 由于 dst 是前置++,所以返回值需要 +1 (数组下标从0开始)
}
3、合并两个有序数组
题目链接
题目描述
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
思路分析
思路:开辟新数组,把数据有序的放入数组中(双指针法):因为两个原数组都是有序的,所以我们可以用两个指针 src1 和 src2 分别指向两个数组,如果src1 指向的元素小于 src2,就把src1的数据放入新数组中然后通过比较大小的方式把数据依次放入新数组中,最后新数组的数据覆盖掉原数组的数据。
思路代码实现
c
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int end1 = m - 1; // 指向数组1最后一个有效数据
int end2 = n - 1; // 指向数组2末尾
int end = m + n - 1; // 指向数组1末尾
while (end1 >= 0 && end2 >= 0) { // 当其中一个数组遍历完成后循环结束
if (nums1[end1] < nums2[end2])
{
// 从后往前,需要找大的
nums1[end--] = nums2[end2--];
}
else
{
nums1[end--] = nums1[end1--];
}
}
// 如果数组1中还有元素则不用管,因为它本来就是有序的
if (end2 >= 0) { // 如果数组2还有元素
while (end2 >= 0) {
nums1[end--] = nums2[end2--];
}
}
}
希望给大家带来帮助!!
一起加油! 我要去西电!!!!!