作者:低调
作者宣言:写好每一篇博客
文章目录
前言
各位读者,大家好!今天我们来讲讲数据结构里面的顺序表,也叫线性表(线性表包括:顺序表、链表、栈、队列、字符串……等等)顺序表有两种形式,一种叫做静态线性表,另一种叫做动态线性表,今天我们主讲动态线性表,以静态的做索引,来实现增删查改等多个接口,话不多说,让我们进入正题:
以下是本篇文章正文内容,下面案例可供参考
一、线性表是什么?
数据结构中实际有两种结构:
1.物理结构(在内存中实际的存储方式)
2.逻辑结构(我们自己想象出来的)
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表通常以数组的形式存储,链表通常以链式结构存储,物理上数组是连续存储的,而链式结构是在内存不同的地方开辟空间,不是连续的。逻辑上数组和链式结构都是我们想象出来成一条直线的结构
1.1顺序表的概念
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
1.静态顺序表:使用定长数组存储。
2.动态顺序表:使用动态开辟的数组存储。
1.2顺序表的静态存储
// 顺序表的静态存储
#define N 100
typedef int SLDataType;//后面用到int的次数很多,方便以后修改
typedef struct SeqList
{
SLDataType array[N]; // 定长数组
int size; // 有效数据的个数
}SeqList;
这是我们实现的一个顺序表,我们需要用结构体来定义一个数组,和计算里面存储了多少个有效数字的变量。
静态顺序表的弊端:
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
接下来我们引入了动态顺序表的概念
二、顺序表的动态存储及其接口实现
我们以存入整型数据为例
// 顺序表的动态存储
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组
int size ; // 有效数据个数
int capicity ; // 容量空间的大小
}SeqList;
通过这个代码跟这个图,可以直观的看到动态的和静态的在本质上没有区别,都是把数字存放到数组中,唯一区别是动态的如果空间不够能够增容,变得灵活。
接下来重点介绍他的所有接口:
我们需要创建两个原文件,和一个头文件,(头文件里面放顺序表的定义,和函数的声明,一个原文件里面放函数的定义,另一个放主函数)
//SeqList.h
// 基本增删查改接口
// 顺序表初始化
void SeqListInit(SeqList* psl);
// 顺序表销毁
void SeqListDestory(SeqList* psl);
// 顺序表打印
void SeqListPrint(SeqList* psl);
// 检查空间,如果满了,进行增容
void CheckCapacity(SeqList* psl);
// 顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x);
// 顺序表尾删
void SeqListPopBack(SeqList* psl);
// 顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x);
// 顺序表头删
void SeqListPopFront(SeqList* psl);
// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErasee(SeqList* psl, size_t pos);
我们要实现的接口还是很多的,不急,我们一一来解决。
2.1顺序表的初始化
注:我们经历了C语言的学习,明白了我们定义了一个变量需要对他进行初始化,防止有随机值,顺序表也一样,需要初始化
直接来看代码:
void SeqListInit(SL* ps)//初始化
{
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * 4);
if (ps->a == NULL)
{
printf("申请内存失败\n");
exit(-1);
}
ps->size = 0;
ps->capacity = 4;
}
注意:我这里还是不放心,因为顺序表是关于数组的动态赖皮,对于数组,我相信你们应该都会了,malloc是关于动态内存管理的知识。
这里的函数给大家介绍malloc怎么使用,千万不能传错参数,里面是字节。
2.2顺序表的销毁
void SeqListDistory(SL* ps)//内存释放
{
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
我们在释放开辟的空间后,一定要将其赋值为空指针,不然会出现野指针的情况,造成程序中断的情况。
相信大家在学习动态内存管理的时候就应该知道free的重要性了吧,这里我给大家在回忆一下,动态分配的内存,实际在堆上,系统没法自动帮你去释放堆上的内存,需要你自己写free或者delete来告诉操作系统需要帮你去释放堆上哪个位置的内存。
虽然在我们平时的学习中,忘记写free了,可能没什么关系,编译器会帮你释放点,但在以后工作中,对于几十万行代码而言,忘记释放自己开辟的空间,可能会造成内存泄漏,这非常严重的问题,为了避免这样的事情的发生,我们要养成释放开辟空间的习惯。
2.3顺序表的打印
void SeqListPrint(SL* ps)//打印
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
这不就是纯纯的数组打印的方式嘛,这我也不做过多的介绍了。
2.4顺序表的扩容
void SelListCheckCapacity(SL* ps)//对已经开辟的空间进行扩容
{
if (ps->size >=ps->capacity)
{
ps->capacity *= 2;
ps->a=(SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity);
if (ps->a == NULL)//防止对空指针的解引用
{
printf("扩容失败\n");
exit(-1);
}
}
}
realloc函数是调整动态开辟空间的大小,他的用法和malloc还有点不同,通过msdn查找:
第一个参数是已经开辟好的空间,后一个一个参数是要开多少个字节的空间,他不单纯的在已经的空间后面追加空间,而是重新找一个新的内存区域开辟一块满足需求的空间,并且把原来内存中的数据拷贝过来并释放之前的空间内存空间,最后返回新开辟的内存空间地址
所以也可以这么改代码:
void SelListCheckCapacity(SL* ps)//对已经开辟的空间进行扩容
{
if (ps->size >=ps->capacity)
{
ps->capacity *= 2;
SLDataType*tmp=(SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity);
if (ps->a == NULL)//防止对空指针的解引用
{
printf("扩容失败\n");
exit(-1);
}
else
{
ps->a=tmp;
}
}
}
这样更能体现realloc找一块新内存空间的含义,两种写法有细微的差别,但达到的效果是完全一样的。
2.5顺序表的尾插
void SeqListPushBack(SL* ps, SLDataType x)//尾插
{
assert(ps);
SelListCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
注:每一次插入时都要检查是否扩容。防止越界访问了。
2.6顺序表的尾删
void SeqListPopBack(SL* ps )//尾删
{
assert(ps);
ps->size--;
}
尾删先对简单些,直接把最后一个有效数字减掉22,到时候就打印不出来这个数,也就进行了删除。
2.7顺序表的头插
void SeqListPushFront(SL* ps, SLDataType x)//头插
{
assert(ps);
SelListCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
头插相对来说麻烦点,我们要把数字插入第一位,那么从第二位到后面的数都要一次往后挪一位,我们思考一下,我们直接把第二位挪到第三位,再把第三位的数挪到第四位,依次这么挪吗?接下来我给大家画个图理解一下这种办法
通过这个图再来看看代码为什么这么写。
2.8顺序表的头删
void SeqListPopFront(SL* ps)//头删
{
assert(ps);
int start = 0;
while (start <ps ->size - 1)
{
ps->a[start] = ps->a[start + 1];
start++;
}
ps->size--;
}
大家可以参考上面的图自己来分析一下,挪动数据倒低怎么挪动比较合适,相信你的脑袋瓜子一定可以思考出来。
2.9顺序表的查找
int SeqListSearch(SL* ps,SLDataType x)//查找某一个数的位置
{
assert(ps);
int i = 0;
while (i < ps->size)
{
if (ps->a[i] == x)
{
return i;
}
++i;
}
return -1;
}
这就相当于数组中的查找,找到了返回下标,找不到就返回-1;
2.10顺序表在pos位置插入x
这个是建立在查找的基础上,这里是在pos前的位置插人,我们可以简单理解,pos前的数字不需要动,把pos看作第一个数,然后进行头插。
void SeqListInsert(SL* ps, int pos, SLDataType x)//任意位置插入
{
assert(ps);
assert(pos <=ps->size&&pos>=0);
SelListCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
思考:在pos后进行插入怎么去实现?
2.11顺序表在pos处进行删除
我们也可以简单理解,pos前面的数字不动,pos看作第一个元素,进行头删,上面的头删理解了,这个代码也就理解了。
void SeqListErase(SL* ps, int pos)//任意位置删除
{
assert(ps);
assert(pos <=ps->size&&pos>=0);
int start = pos;
while (start < ps->size - 1)
{
ps->a[start] = ps->a[start + 1];
start++;
}
ps->size--;
}
三、顺序表的运行结果展示
#define _CRT_SECURE_NO_WARNINGS
#include "SeqList.h"
void TestSeqList1()
{
SL s;
SeqListInit(&s);//初始化数组
SeqListPushBack(&s, 1);//尾插
SeqListPushBack(&s, 2);//尾插
SeqListPushBack(&s, 3);//尾插
SeqListPushBack(&s, 4);//尾插
SeqListPushBack(&s, 5);//尾插
SeqListPushBack(&s, 6);//尾插
SeqListPushBack(&s, 7);//尾插
printf("尾插后的结果:");
SeqListPrint(&s);
SeqListPopBack(&s);//尾删
printf("尾删后的结果:");
SeqListPrint(&s);
SeqListPushFront(&s, -1);//头插
SeqListPushFront(&s, -2);//头插
printf("头插后的结果:");
SeqListPrint(&s);
SeqListPopFront(&s);//头删
SeqListPopFront(&s);//头删
printf("头删后的结果:");
SeqListPrint(&s);
int pos = SeqListSearch(&s, 4);
printf("查找的位置是:%d\n",pos);
SeqListInsert(&s, pos, 10);//任意位置插入
printf("从pos位置前插入的结果:");
SeqListPrint(&s);
SeqListErase(&s, pos);//任意位置删除
printf("从pos位置删除的结果:");
SeqListPrint(&s);
SeqListDistory(&s);//内存释放
//free(s.a);//这个也可以,尽量包装成函数
//s.a = NULL;
}
int main()
{
TestSeqList1();
return 0;
}
四、顺序表的细节补充
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
这里是需要的头文件,#pragma once是防止头文件被重复包含使用
我们两个原文件只需要引用自己的头文件即可
#include "SeqList.h"
我们可以检查自己的任意插入和任意删除是否正确,采取用头插,头删,尾插,尾删来调用任意插入或者删除,读者可以自己下去测试一下这个接口
void SeqListPushBack(SL* ps, SLDataType x)//尾插
{
SeqListInsert(ps,ps->size-1,x);
}
void SeqListPopBack(SL* ps )//尾删
{
SeqListErase(ps,ps->size-1);
}
void SeqListPushFront(SL* ps, SLDataType x)//头插
{
SeqListInsert(ps,0,x);
}
void SeqListPopFront(SL* ps)//头删
{
SeqListErase(ps,0);
}
注意:我们进行顺序表的增删查改时,是要改变他的内容,所以传参需要传址。
顺序表
1.可动态增加的数组。
2.数据在数组的存储时必须时连续的
优点:
1.支持随机访问。
2.缓存命中率高。
点开链接了解缓存命中率:link
缺点:
1.中间过着头部的插入删除需要挪动数据,时间复杂度时O(N);
2.空间不够时,增容会有一定的消耗和空间的浪费;
五、oj题练习
移除元素
传统想法:在创建一个新数组,在原数组进行一一遍历,不等于val放到新数组中,在返回新数组的地址,就可以满足要求。但此题明确说明了不能创建新的数组,
原地修改数组,这时候怎么办呢,我们先来看代码:
int removeElement(int* nums, int numsSize, int val)
{
int i,j=0;
for(i=0;i<numsSize;i++)
{
if(val!=nums[i])
{
nums[j++]=nums[i];
}
}
return j;
}
注:看着代码理解一下,我们直接对着原数组进行修改,没有创建新数组。点开链接做题尝试一下吧。link
总结
通过这篇博客,相信大家对顺序表又进一步的了解了,我们自己实现了他的增删查找多个接口,如果大家对顺序表很了解的情况下,对于接下来的链表也有很大帮助,希望大家可以关注博主,我会继续更新有关数据结构方面的知识,方便大家的理解和学习的,让我们下一篇博客再见!!