目录
一、线性表
- 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是⼀种在实际中⼴泛使⽤的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
- 线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
- 顺序表一定是线性表,但线性表不一定是顺序表。
- 逻辑结构:指认为想象出来的数据的组织形式,线性表的逻辑结构都是线性的;
- 物理结构:指数据在内存上的存储形式,线性表的物理结构不一定是线性的。
二、顺序表
1、概念与结构
2、分类
1.静态顺序表
typedef int SLDataType;
#define N 7
typedef struct SepList
{
SlDataType a[N];
int size;
}SL;
2.动态顺序表
动态顺序表可以根据需求而去内存中自由增容。
//动态顺序表--按需申请
typedef struct SepList
{
SlDataType* a;
int size;//有效数据个数
int capacity;//空间容量
}SL;
3、动态顺序表的实现
学习数据结构时,可将代码分成头文件和源文件这两大类文件,这样方便边写代码边测试检查有没有错误,实现各种用途。
前提:
- 必须得传指针,而不是传值。
- 传值:形参是实参的值的拷贝,实参保存的值拷贝一份给形参,实参和形参指向的是两块不同的地址,但保存的数据是一样的。
- 传址:形参指向的就是实参指向的地址。
- 若是传值而不传地址的话,会出现报错的提示,如下:
动态顺序表主要包括以下这几种功能的实现:
定义动态顺序表的结构体、初始化、销毁、打印、判断空间是否充足、尾插、尾删、头插、头删、指定位置插入数据、指定位置删除数据、查找。
学习以下的命名方式和具体函数实现的写法,可为将来学习C++中的stl知识部分保持一致。
重点实现方法:
1、定义动态顺序表的结构体
#include<stdio.h>
#include<stdlib.h>
//定义结构体需要的头文件
//定义动态顺序表结构
typedef int SLDataType;
// 动态顺序表 -- 按需申请
typedef struct SeqList
{
SLDataType* arr;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
//上述的结构体定义也可以像这样写
//typedef struct SeqList Sl;
思路要点:
第一、typedef关键字在结构体的运用:
typedef 是用来类型重命名的,可以将复杂的类型,简单化。(详细可到博主的C_深入理解指针(三)中详细介绍的typedef关键字http://t.csdnimg.cn/lJdAZ)
例如:上述的定义动态顺序表的结构体SeqList重命名为SL;重新命名int类型为SLDataType
第二、定义结构体需要的头文件为 #include<stdlib.h>,不能漏写。
2、初始化
//头文件SeqList.h
//初始化函数定义
void SLInit(SL* ps);
//下面是源文件
//实现的项目文件SepList.c
#include<SepList.h>
//初始化函数
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
}
int main()
{
SLtest01();
return 0;
}
思路要点:
- 初始化将结构体中的变量置位为原始值,即顺序表的指针置为指向顺序表的第一个元素,有效数据个数和空间容量置为0。
- 传指针不能传值,解引用使用的是箭头来表示,通俗易懂。
3、销毁
//头文件SeqList.h
//销毁
void SLDestroy(SL* ps);
//下面是源文件
//实现的项目文件SepList.c
#include<SepList.h>
//销毁
void SLDestroy(SL* ps)
{
if(ps->arr)//相当于ps->arr!=NULL
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}
思路要点:
- 初始化之后,经过增删查改,需要释放和销毁所在内存创建的空间,不让内存被占用。
- 销毁函数的实现,首先要判断顺序表的指针是否为空指针,是的话则重复初始化的操作,即将结构体中的变量置位为原始值,即顺序表的指针置为指向顺序表的第一个元素,有效数据个数和空间容量置为0;否的话则先释放所在内存创建的空间,再重复初始化的操作。
- 初始化和销毁是顺序表的必要操作,不可或缺,必须放在各放在首尾分别实现,不然顺序表不能完成最基本的增删查改操作。
4、打印
//头文件SeqList.h
//打印
void SLPrint(SL* ps);
//下面是源文件
//实现的项目文件SepList.c
#include<SepList.h>
//打印函数
void SLPrint(SL* ps)
{
//使用基本的循环和解引用打印顺序表的各个元素
for(int i = 0;i < ps->size; i++)
{
printf("%d ",ps->arr[i]);
}
printf("\n");
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
SLPrint(&s);
}
int main()
{
SLtest01();
return 0;
}
5、判断空间是否充足
//头文件SeqList.h
//判断空间是否充足,再扩容
void SLCheckCapacity(SL* ps);
//下面是源文件
//实现的项目文件SepList.c
//判断空间是否足够
void SLCheckCapacity(SL* ps)
{
//判断空间是否充足
if(ps->size == ps->capacity)
{
//增容//0*2 = 0
//若capacity为0,给个默认值,否则×2倍
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity*sizeof(SLDatatype));
if(tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity *= 2;
}
}
//判断空间是否足够——一般是运用到增删查改这些函数实现中的,所以test.c测试文件项目运用在这些方面,这里就不显示了
思路要点:
- size指向的是顺序表最后一位元素的下一位,而capacity是指向表示顺序表的空间容量的大小,通过比较两者的大小是否相等,来判断空间大小是否充足。
- 若size和capacity两者相等,意味着空间大小不够,则判断需要增容。
- 增容首先要先判断是capacity是否为0(即为初始值),若为0,首先通过问号表达式使capacity赋值为4(默认值);否则将原来的空间大小×2;最后赋值给一个新的表示空间大小容量的newCapacity。
- 增容空间大小确定后,使用realloc函数在指向原来顺序表的首元素地址处进行增容,空间容量×sizeof(所选数据类型大小),再强制类型转化为结构体指针变量类型。
- 新的结构体指针变量需要判断是否为空,即为检查是否realloc增容成功,若为空,则增容失败,退出程序;若不为空,则表示增容成功。
- 接下来的尾插、尾删、头插、头删、指定位置插入数据、指定位置删除数据和查找等顺序表的主要重要的函数实现内容都会使用到判断空间是否充足。
增容分两种情况 | 增容一般是成倍数的增加,比如2倍、3倍...... |
第一种情况: | 第二种情况: |
连续空间足够,直接扩容 | 连续空间不够,则需要: (1)重新找一块地址,分配足够的内存 (2)拷贝数据到新的地址 (3)销毁旧地址 |
思考:为何不可以一次增加一个呢?这样不就没有空间浪费了嘛?
增容的操作本身就有一定的程序性能的消耗,若频繁的增容会导致程序效率低下。这就涉及到概率学的知识,增量和插入数据的个数是正相关的。
6、尾插
//头文件SeqList.h
//尾插
void SLPushBack(SL* ps, SLDataType x);
//下面是源文件
//实现的项目文件SepList.c
//判断空间是否足够
void SLCheckCapacity(SL* ps)
{
//判断空间是否充足
if(ps->size == ps->capacity)
{
//增容//0*2 = 0
//若capacity为0,给个默认值,否则×2倍
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity*sizeof(SLDatatype));
if(tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity *= 2;
}
}
//尾插
void SLPushBack(SL* ps,SLDatatype x)
{
//粗暴的解决方法——断言
assert(ps);//等价于assert(ps != NULL);
温柔的解决方式
//if(ps == NULL)
//{
// return;
//}
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
//尾插
SLPushBack(&s,1);
SLPushBack(&s,2);
SLPushBack(&s,3);
SLPushBack(&s,4);
SLPushBack(NULL,4);//尾插传空指针的情况
SLPrint(&s);
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}
思路要点:
当尾插传空指针的情况时,使用assert断言判断条件,如不符合判断条件会出现:
引用上一有关算法复杂度的博客,可以判断出——尾插的时间复杂度:O(1)
尾插空间判断:
- 空间充足:将数据插入到size指向的位置,size的位置是指向数组最后一个元素的下一位,然后再size++
- 空间不足:将数据复制一份,再重新在内存中申请开辟足够的内存空间,将数据拷贝到新开辟的内存空间中,再销毁旧地址。
7、尾删
//头文件SeqList.h
//尾删
void SLPopBack(SL* ps);
//下面是源文件
//实现的项目文件SepList.c
//尾删
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);
//ps->arr[ps->size-1] = -1;//多余了
ps->size--;//size--后,原本的size位置的数据就不为有效数据了,可直接覆盖
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
//尾插
SLPushBack(&s,1);
SLPushBack(&s,2);
SLPushBack(&s,3);
SLPushBack(&s,4);
SLPushBack(NULL,4);//尾插传空指针的情况
SLPrint(&s);
//尾删
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPrint(&s);//删完之后对空的顺序表再尾删会报错
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}
思路要点:
根据size的特性,size为指向数组最后一个元素的下一位,size之前都是有效数据,size--之后,原本的size位置的数据就不为有效数据了,可以直接覆盖。
根据算法复杂度,可知,尾删的时间复杂度为O(1)
当尾删传空指针的情况时,使用assert断言判断条件,如不符合判断条件会出现:
8、头插
//头文件SeqList.h
//头插
void SLPushFront(SL* ps, SLDataType x);
//下面是源文件
//实现的项目文件SepList.c
//判断空间是否足够
void SLCheckCapacity(SL* ps)
{
//判断空间是否充足
if(ps->size == ps->capacity)
{
//增容//0*2 = 0
//若capacity为0,给个默认值,否则×2倍
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity*sizeof(SLDatatype));
if(tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity *= 2;
}
}
//头插
void SLPushFront(SL* ps,SLDatatype 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++;
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
//头插
SLPushFront(&s,1);
SLPushFront(&s,2);
SLPushFront(&s,3);
SLPushFront(&s,4);
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}
思路要点:
首先判断结构体是否为空和所插入的空间是否充足,再通过for循环将原数组数据整体后移一位,将下标为0的位置空出来,再插入新的data值,size有效元素个数++。
根据算法复杂度来看,可知,头插的时间复杂度:O(n)
当头插传空指针的情况时,使用assert断言判断条件,如不符合判断条件会出现:
9、头删
//头文件SeqList.h
//头删
void SLPopFront(SL* ps);
//下面是源文件
//实现的项目文件SepList.c
//头删
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);
//数据整体向前挪动一位
for(int i = 0;i<ps->size-1;i++)
{
ps->arr[i] = ps->arr[i+1];//i=size-2
}
ps->size--;
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
//头插
SLPushFront(&s,1);
SLPushFront(&s,2);
SLPushFront(&s,3);
SLPushFront(&s,4);
//头删
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);//删完之后对空的顺序表再头删会报错
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}
思路要点:
头删将第一个位置以后的数据直接整体向前挪动一位,同时size--,将有效数据置为前一位数据,整体前移直接覆盖头部的数据,完成头删函数实现操作。
根据算法复杂度,可知,头删时间复杂度为O(n)
当头删传空指针的情况时,使用assert断言判断条件,如不符合判断条件会出现:
10、指定位置插入数据
//头文件SeqList.h
//指定位置插⼊数据
void SLInsert(SL* ps, int pos, SLDataType x);
//下面是源文件
//实现的项目文件SepList.c
//判断空间是否足够
void SLCheckCapacity(SL* ps)
{
//判断空间是否充足
if(ps->size == ps->capacity)
{
//增容//0*2 = 0
//若capacity为0,给个默认值,否则×2倍
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity*sizeof(SLDatatype));
if(tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity *= 2;
}
}
//指定位置之前插⼊数据(空间足够才能直接插入数据)
void SLInsert(SL* ps, int pos, SLDataType 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];//pos+1->pos
}
ps->arr[pos] = x;
ps->size++;
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
//指定位置插入数据
SLInsert(&s,11,0);
SLPrint(&s);
SLInsert(&s,22,s.size);
SLPrint(&s);
SLInsert(&s,33,2);
SLPrint(&s);
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}
思路要点:
assert断言判断条件中的前边界限等价于为头插位置,后边界限等价于为尾插位置。 pos为所选指定位置,通过for循环将pos及之后的数据整体向后移动一位,使pos位置空出来,将所想要插入的值插进去,最后将size++,使有效的数据+1。
根据算法复杂度,可知,指定位置插入数据的时间复杂度不确定,但空间复杂度可根据数组的具体长度来判断算法复杂度的多少,详细可见博主上一篇博客的算法复杂度中的查找字符串部分(http://t.csdnimg.cn/JpVmg)。
指定位置插入数据的运行结果:
11、指定位置删除数据
//头文件SeqList.h
//指定位置删除数据
void SLErase(SL* ps, int pos);
//下面是源文件
//实现的项目文件SepList.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];//size-2 <- size-1
}
ps->size--;
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
//指定位置插入数据
SLInsert(&s,11,0);
SLPrint(&s);
SLInsert(&s,22,s.size);
SLPrint(&s);
SLInsert(&s,33,2);
SLPrint(&s);
//指定位置删除
SLErase(&s,0);
SLPrint(&s);
SLErase(&s,s.size-1);
SLPrint(&s);
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}
思路要点:
assert断言判断条件中的前边界限等价于为头插位置,后边界限等价于为尾插位置。 pos为所选指定位置,通过for循环将pos之后的数据整体向前挪动一位,使pos位置原来的值被覆盖,,最后将size--,使有效的数据-1;其指定位置删除数据的算法复杂度同上 。
12、查找
//头文件SeqList.h
//查找
int SLFind(SL* ps, SLDataType x);
//下面是源文件
//实现的项目文件SepList.c
//查找
int SLFind(SL* ps,SLDatatype x)
{
assert(ps);
//循环遍历数组查找
//其时间复杂度不确定,但可算空间复杂度
for(int i = 0; i < ps->size;i++)
{
if(ps->arr[i] == x)
{
return i;
}
}
//没有找到:返回一个无效的下标
return -1;
}
//测试的项目文件tset.c
#include<SepList.h>
void SLtest01()
{
SL s;
SLInit(&s);
//查找
int find = SLFind(&s,2);
if(find < 0)
{
printf("没有找到!\n");
}
else
{
printf("找到了!\n");
}
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}
4、完整实现动态内存管理的三个文件
SeqList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义动态顺序表结构
typedef int SLDataType;
// 动态顺序表 -- 按需申请
typedef struct SeqList
{
SLDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
//上述的结构体定义也可以像这样写
//typedef struct SeqList Sl;
//初始化
void SLInit(SL* ps);
//销毁
void SLDestroy(SL* ps);
//打印
void SLPrint(SL* ps);
//判断空间是否充足,再扩容
void SLCheckCapacity(SL* ps);
//尾插
void SLPushBack(SL* ps, SLDataType x);
//尾删
void SLPopBack(SL* ps);
//头插
void SLPushFront(SL* ps, SLDataType x);
//头删
void SLPopFront(SL* ps);
//指定位置插⼊数据
void SLInsert(SL* ps, int pos, SLDataType x);
//指定位置删除数据
void SLErase(SL* ps, int pos);
//查找
int SLFind(SL* ps, SLDataType x);
SeqList.c
#include<SepList.h>
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//销毁
void SLDestroy(SL* ps)
{
if(ps->arr)//相当于ps->arr!=NULL
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//判断空间是否足够
void SLCheckCapacity(SL* ps)
{
//判断空间是否充足
if(ps->size == ps->capacity)
{
//增容//0*2 = 0
//若capacity为0,给个默认值,否则×2倍
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity*sizeof(SLDatatype));
if(tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity *= 2;
}
}
//打印函数
void SLPrint(SL* ps)
{
for(int i = 0;i < ps->size; i++)
{
printf("%d ",ps->arr[i]);
}
printf("\n");
}
//尾插
void SLPushBack(SL* ps,SLDatatype x)
{
//粗暴的解决方法——断言
assert(ps);//等价于assert(ps != NULL);
温柔的解决方式
//if(ps == NULL)
//{
// return;
//}
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
//尾删
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);
//ps->arr[ps->size-1] = -1;//多余了
ps->size--;//size--后,原本的size位置的数据就不为有效数据了,可直接覆盖
}
//头插
void SLPushFront(SL* ps,SLDatatype 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++;
}
//头删
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);
//数据整体向前挪动一位
for(int i = 0;i<ps->size-1;i++)
{
ps->arr[i] = ps->arr[i+1];//i=size-2
}
ps->szie--;
}
//指定位置之前插⼊数据(空间足够才能直接插入数据)
void SLInsert(SL* ps, int pos, SLDataType 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];//pos+1->pos
}
ps->arr[pos] = 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];//size-2 <- size-1
}
ps->size--;
}
//查找
int SLFind(SL* ps,SLDatatype x)
{
assert(ps);
for(int i = 0; i < ps->size;i++)
{
if(ps->arr[i] == x)
{
return i;
}
}
//没有找到:返回一个无效的下标
return -1;
}
test.c
#include<SeqList.h>
void SLtest01()
{
SL s;
SLInit(&s);
//尾插
SLPushBack(&s,1);
SLPushBack(&s,2);
SLPushBack(&s,3);
SLPushBack(&s,4);
SLPushBack(NULL,4);//尾插传空指针的情况
//头插
SLPushFront(&s,1);
SLPushFront(&s,2);
SLPushFront(&s,3);
SLPushFront(&s,4);
SLPrint(&s);
//尾删
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPrint(&s);
SLPopBack(&s);
SLPrint(&s);//删完之后对空的顺序表再尾删会报错
//头删
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);//删完之后对空的顺序表再头删会报错
//指定位置插入数据
SLInsert(&s,11,0);
SLPrint(&s);
SLInsert(&s,22,s.size);
SLPrint(&s);
SLInsert(&s,33,2);
SLPrint(&s);
//指定位置删除
SLErase(&s,0);
SLPrint(&s);
SLErase(&s,s.size-1);
SLPrint(&s);
//查找
int find = SLFind(&s,2);
if(find < 0)
{
printf("没有找到!\n");
}
else
{
printf("找到了!\n");
}
SLDestroy(&s);
}
int main()
{
SLtest01();
return 0;
}