数据结构学习笔记二线性表---顺序表篇(画图详解+代码实现)

Posted 大家好我叫张同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构学习笔记二线性表---顺序表篇(画图详解+代码实现)相关的知识,希望对你有一定的参考价值。


本篇文章及后面几篇文章将会详细介绍和学习数据结构线性表中的顺序表和链表,这两种数据结构将是学习其他数据结构的基础。
文章内容结构如下:
①理论基础
②画图详细讲解
③代码实现
④常见oj题刷题



线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

理解: 数据结构实际上分两种结构:
1、物理结构(内存中如何存放的)
2、逻辑结构(是我们想象出来的)

在之前的学习中,我们提及到程序中内存区域的划分,如图:

如果我们按照物理结构将数据结构进行分类,实际上仅有两种:
①物理结构上,数据连续存储的-- - 数组(物理上连续,逻辑上也连续,缺点:数组大小固定,可能导致内存浪费)
②物理结构上,数据不连续存储的-- - 链式结构(物理上不连续,逻辑上连续,优点:内存可以按需分配)

线性:类似于一条直线,一个元素连接另外一个元素,具有连续性。
线性表的线性指的是逻辑结构上的线性,而不是物理结构上的线性,


顺序表

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。(基于对数据操作的位置,将其分为头部、尾部、中间 / 任意位置)

理解:顺序表是线性表其中的一种,顺序表实际上就是数组的一种应用,C语言写顺序表的时候一般喜欢命名为SeqList,而在C++中名称为Vector(向量)

顺序表与数组的区别和联系?

(1)顺序表是在计算机内存中以数组的形式保存的线性表。
(2)顺序表是指用一组地址连续的存储单元依次存储数据元素的线性结构。线性表采用顺序存储的方式存储就称之为顺序表,顺序表是将表中的结点依次存放在计算机内存中一组地址连续的存储单元中。线性表采用指针链接的方式存储就称之为链表。
(3)线性表是从逻辑结构的角度来说的,除了头和尾之外,它的每一个元素都只有一个前驱元素和一个后驱元素。各种队列(单向、双向、循环队列),栈等都是线性表的不同例子。
(4)而数组是从物理存贮的角度来说的,线性表可以用数组存贮也可以用链表来存贮。同样的队列和栈也可以用数组和链表存贮,各有利弊。具体使用时,根据具体情况选择。
所以说,数组是一个更大的概念。使用数组,不但可以存储线性表,也可存储非线性结构的数据结构。比如堆、完全二叉树、乃至于其它类型的树、图等。

总结

顺序表与数组都是数据结构,只是描述角度不同。顺序表是从逻辑结构的角度来说的,它的每一个元素都只有一个前驱元素和一个后驱元素除了头和尾,逻辑结构还有队列,堆栈,树,图等。而数组是从物理存贮的角度来说的,顺序表用数组存贮也可以用链表来存贮。同样的队列也可以用数组和链表存贮,各有利弊。具体使用时,根据具体情况选择。


顺序表一般可以分为 :

1.静态顺序表 : 使用定长数组存储。
2.动态顺序表 : 使用动态开辟的数组存储。

静态顺序表的实现

SeqList.h 头文件:函数声明模块

#pragma once
#include<stdio.h>
#include<assert.h>

//最初版本
//struct SeqList
//
//	int data[100];//一个定长的数组
//	struct SeqList* next;//指向下一个结点的地址的指针
//;

//进行优化
//1.这个结构体中的数据类型是int,这样就将数组的类型局限了,可能后面我们需要处理char、double或各种类型的数据
//为了数组类型的通用,我们可以将这个int进行类型重定义,后面如果有涉及到数据类型的修改,直接改动重定义部分的类型
//不涉及到结构体内数组元素类型的修改---便携性和通用性
//2.基于同样的道理,数组的大小100在结构体内局限性太强,为了方便数组大小实际应用过程中的调整
//可以将100用#define N,宏命令---便携性和通用性
//3.定义的结构体类型是 struct SeqList 这个类型名称太长,不方便我们后续写代码,可以对其进行类型重定义

//优化版本

#define N 100
typedef int SLDataType;//SL是SeqLsit的缩写,DataType表示数据类型

typedef struct SeqList

	SLDataType data[N];//定长数组
	//struct SeqList* next;//或者也可以用 SeqList* next;
	size_t size;//有效数据的个数
SeqList;

//初始化
void SeqListInit(SeqList* pList);

//尾插
void SeqListPushBack(SeqList* pList, SLDataType x);

//尾删
void SeqListPopBack(SeqList* pList);

//打印
void SeqListPrint(SeqList* pList);

思考:顺序表为什么习惯用SeqList命名?有什么具体含义或者背景吗?

sequence 先后次序,顺序,连续 SeqList应该是Sequence + List 的缩写(方便理解和记忆)

SeqList.c 源文件,函数实现模块

#include"SeqList.h"
//初始化
void SeqListInit(SeqList * pList)

	assert(pList);
	pList->data[N] = 0;
	pList->size = 0;


//尾插
void SeqListPushBack(SeqList* pList, SLDataType x)

	assert(pList);
	//1.首先考虑是否有空间给数据插入
	if (pList->size == N)
	
		printf("SeqList is full!\\n");
	
	//2.有空间就往后面插入数据
	else
	
		int i = pList->size;
		pList->data[i] = x;
		pList->size++;
	


//尾删
void SeqListPopBack(SeqList* pList)

	assert(pList);
	//1.首先判断是否还存在可以删除的元素
	if (pList->size == 0)
	
		printf("SeqList is empty!\\n");
		printf("Delete date failure!\\n");
	
	else
	
		pList->size--;
	


//打印
void SeqListPrint(SeqList* pList)

	assert(pList);
	size_t i = 0;
	if (pList->size == 0)
	
		printf("SeqList is empty!\\n");
	
	else
	
		for (i = 0; i < pList->size; i++)
		
			printf("%d ", pList->data[i]);
		
	

test.c 测试模块

#define _CRT_SECURE_NO_WARNINGS 1 
#include"SeqList.h"
SeqList;
void SeqListTest1()

	SeqList SL;
	SeqList* pList = &SL;
	SeqListInit(pList);
	SeqListPrint(pList);

	SeqListPushBack(pList, 1);
	SeqListPushBack(pList, 2);
	SeqListPushBack(pList, 3);
	SeqListPushBack(pList, 4);

	SeqListPrint(pList);
	printf("\\n_______________________\\n");

	SeqListPopBack(pList);
	SeqListPopBack(pList);
	SeqListPopBack(pList);
	//SeqListPopBack(pList);
	//SeqListPopBack(pList);

	SeqListPrint(pList);
	printf("\\n_______________________\\n");


int main()

	SeqListTest1();
	return 0;


静态存储方式的顺序表分析:
1、由于静态顺序表在创建的时候就已经确定了数组的大小且中途无法更改其大小,那么势必会存在以下问题:
最初的大小给多少合适呢?
如果数组大小给小了,后面在使用的时候就会导致一部分数据无法存储,这肯定是不行的。
如果数组大小给大了,那在后续的使用中会存在存储空间的浪费。(在实际使用之前,可能我们也不知道数据的规模和大小,所以这种静态结构的顺序表局限性很大)
2、由于物理位置的连续性,数据的头插和中间任意位置插入不方便,通常需要将后面的其它元素整体移动,头插\\中间插入数据的时间复杂度为O(N)。

为了解决第一个问题-- - 数组大小无法更改,引入了动态顺序表,即使用动态开辟的数组存储数据,可以在一定程度上解决数组大小无法更改的问题,仍然可能存储空间浪费的问题,但是整体而言相对于静态顺序表来说是有所提升的。所以在实际中,动态顺序表的使用频次远比静态顺序表要多,接下来我们来详细学习动态顺序表。


顺序表的基本增删查改接口有:

顺序表的初始化、销毁、打印、尾插、尾删、头插、头删、查找、增容、在任意位置(pos位置)插入、在任意位置(pos位置)删除,判空,求长度等

注意:值传递的方式,函数调用之后,外面的值不会发生改变,而且因为这种方式还会造成内存泄漏(malloc) 应该改成传址的形式

思考:为什么一般只进行增容操作,在数据删除后不进行缩容(缩减capacity)的操作?

个人理解:虽然反复调整capacity的大小可以实现内存空间的动态缩小和扩大,但是反复增容缩容本身很容易造成内存的碎片化,同时增容缩容本身也是占用一定的程序运行时间的,反复调整也会影响程序的整体性能。整体相较而言,虽然内存空间有所节省,但是带来的麻烦远大于收益。

思考:为什么常见的增容方式都是按照2倍,3倍,n倍去增容,为什么不一次增加一个空间,这样不是更加节省空间吗?

个人理解:增容本身也是占用一定的程序运行时间的,反复调整也会影响程序的整体性能。按照倍数增容的好处是,数据量级越小,增容次数越少,数据量级越多,到后期增容的次数也不会很多,因为越往后,增容增加的空间越多。


动态顺序表算法图解

尾插算法思想图解:

尾删算法思想图解:

头插算法思想图解:

头删算法思想图解:

在任意位置插入数据算法思想图解:

在任意位置删除数据算法思想图解:

提示:插入数据要判满,删除数据要判空

只要是涉及到数据的插入,都有可能导致数据元素的个数超过顺序表的容量,因此需要进行扩容检查,也就是“判满”。同样,只要是涉及到数据的删除,就都需要先检查顺序表是否有数据可以删除,是否为空,就是“判空”。


实际上,当我们写出来在任意位置的插入和删除代码时,头删尾删,头插尾插内部实现的时候不用分模块各自实现,可以直接调用在任意位置插入删除的函数来实现,这样就可以实现代码的高效复用-- - 可以大大提高编程的效率和代码的复用率!(相同的一段代码或者函数,在同一个编程项目中使用的次数越多,其复用率越高,编程的质量也越好)

动态顺序表的实现

SeqList_Dynamic.h 头文件

#pragma once
#include<stdio.h>
#include<stdbool.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLDataType;
typedef struct SeqList 
	SLDataType* arr;
	int size;
	int capacity;
SeqList;
void SeqListInit(SeqList* pList);//初始化
void SeqListDestory(SeqList* pList);//销毁
void SeqListPushFront(SeqList* pList,SLDataType x);//头插
void SeqListPopFront(SeqList* pList);//头删
void SeqListPushBack(SeqList* pList,SLDataType x);//尾插
void SeqListPopBack(SeqList* pList);//尾删
void SeqListInsert(SeqList* pList,int pos,SLDataType x);//任意位置插入
void SeqListErase(SeqList* pList,int pos);//任意位置删除
int SeqListFind_By_Val(SeqList* pList,SLDataType x);//查找
void SeqListUpdata(SeqList* pList,int pos,SLDataType x);//修改
void SeqListPrint(SeqList* pList);//打印
bool SeqListIsEmpty(SeqList* pList);//判空
int SeqListLength(SeqList* pList);//长度

SeqList_Dynamic.c 源文件

#include"SeqList_Dynamic.h"
//初始化
void SeqListInit(SeqList* pList) 
	pList->arr = (SLDataType*)malloc(2 * sizeof(SLDataType));
	pList->capacity = 2;
	pList->size = 0;

//销毁
void SeqListDestory(SeqList* pList) 
	free(pList->arr);
	pList->arr = NULL;
	pList->size = 0;
	pList->capacity = 0;

//扩容检查
void CheckCapacity(SeqList* pList) 
	assert(pList);
	if (pList->size >= pList->capacity) 
		int NewCapacity = 2 * (pList->capacity);
		SLDataType* NewArray = (SLDataType*)realloc(pList->arr, NewCapacity * sizeof(SLDataType));
		if (NewArray == NULL) 
			printf("Increase capacity failed!\\n");
			exit(-1);//扩容失败,提前结束进程
		
		//扩容成功
		pList->arr = NewArray;
		pList->capacity = NewCapacity;
		printf("Increase capacity successful!\\n");
	

//头插
void SeqListPushFront(SeqList* pList, SLDataType x) 
	assert(pList);
	CheckCapacity(pList);
	//插入数据,先将所有数据从后往前移动一位
	int i = 0;
	for (i = pList->size; i > 0; i--) 
		pList->arr[i] = pList->arr[i - 1];
	
	pList->arr[i] = x;
	pList->size++;

//头删
void SeqListPopFront(SeqList* pList) 
	assert(pList);
	if (SeqListIsEmpty(pList) == 1) 
		printf("SeqList is empty!\\n");
		return 0;
	
	//删除数据,直接将数据从前往后移动覆盖
	int i = 0;
	for (i = 0; i < pList->size - 1; i++) 
		pList->arr[i] = pList->arr[i + 1];
	
	pList->size--;

//尾插
void SeqListPushBack(SeqList* pList, SLDataType x) 
	assert(pList);
	CheckCapacity(pList);
	//插入数据
	pList->arr[pList->size] = x;
	pList->size++;

//尾删
void SeqListPopBack(SeqList* pList) 
	assert(pList);
	if (SeqListIsEmpty(pList) == 1) 
		printf("SeqList is empty!\\n");
		return 0;
	
	pList->size--;

//任意位置插入
void SeqListInsert(SeqList* pList, int pos, SLDataType x) 
	assert(pList);
	CheckCapacity(pList);
	if (pos<0 || pos > pList->size) 
		printf("The position to insert is illegal!\\n");
		exit(-1);
	
	//插入数据,将pos位置及其后面的元素从后往前移动一位
	int i = 0;
	for (i = pList->size; i > pos; i--) 
		pList->arr[i] = pList->arr[i - 1];
	
	pList->arr[i] = x;
	pList->size++;

//任意位置删除
void SeqListErase(SeqList* pList, int pos) 
	assert(pList);
	if (SeqListIsEmpty(pList) == 1) 
		printf("SeqList is empty!\\n");
		return 0;
	
	if (pos < 0 || pos >pList->size) 
		printf("The position to erase is illegal\\n");
		exit(-1);
	
	//删除数据,将pos后面的元素从前往后向前移动覆盖
	for (int i = pos; i < pList->size - 1; i++) 
		pList->arr[i] = pList->arr[i + 1];
	
	pList->size--;

//查找
int SeqListFind_By_Val(SeqList* pList,SLDataType x) 
	assert(pList);
	if (SeqListIsEmpty(pList) == 1) 
		printf("SeqList is empty!  Can't Find Anything!\\n");
		return 0;
	
	int i = 0;
	for (i = 0; i < pList->size; i++) 
		if (pList->arr[i] == x)
			break;
	
	if (i == pList->size)
		printf("Can't find it!\\n");
	else 
		printf("Got it,the index is %d !\\n", i);
		return i;
	

//修改
void SeqListUpdata(SeqList* pList, int pos,SLDataType x) 
	assert(pList);
	if (pos<0 || pos>pList->size) 
		printf("The position is illegal!\\n");
		exit(-1);
	
	pList->arr[pos] = x;

//打印
void SeqListPrint(SeqList* pList) 
	assert(pList);
	//判空
	if (SeqListIsEmpty(pList) == 1) 
		printf("SeqList is empty!\\n");
	for (int i = 0; i < pList->size; i++) 
		if (i != pList->size - 1)
			printf(" %d ---", pList->arr[i]);
		else
			printf(" %d\\n", pList->arr[i]);
	
	printf("------------------------\\n");

//判空
bool SeqListIsEmpty(SeqList* pList) 
	assert(pList);
	if (pList->size == 0)
		return 1;
	else
		return 0;

//长度
int SeqListLength(SeqList* pList) 
	assert(pList);
	return pList->size; 

Test.c

以上是关于数据结构学习笔记二线性表---顺序表篇(画图详解+代码实现)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构学习笔记二线性表之链表篇(双向链表)

数据结构——线性表之顺序表篇

C Language 线性表篇 - 顺序表

C Language 线性表篇 - 顺序串

数据结构与算法学习笔记:线性表Ⅰ

数据结构学习笔记 线性表的顺序存储和链式存储