动态顺序表

顺序表(动态)

顺序表分为静态和动态,静态的顺序表和动态顺序表相关接口的实现差别不大,因此不在赘述。

头文件定义

类型定义

#pragma once
#include 
#include 
#include 

//#define N 10
#define INIT_CAPACITY 4  // 给顺序表一个初始大小

typedef int SLDataType;	//定义顺序表数据类型

静态顺序表   开少了不够用   开多了浪费
//typedef struct SeqList {
//	SLDataType arr[N];
//	int size;
//};

//动态顺序表   动态扩容   开辟内存在堆区       没必要用柔性数组
typedef struct SeqList {
	SLDataType* base;	//动态数组的基地址
	int size;		//有效数据个数		size是    最后一个元素的下一个位置的下标
	int capacity;	//空间容量
}SeqList;

此处阐释静态顺序表的劣势。
静态顺序表开辟的空间是固定的,开辟空间过少,不够存储, 开辟空间过多, 造成空间的浪费。因此在实际使用的过程中,使用动态顺序表居多。

使用指针管理顺序表的地址, 利用C语言的malloc函数和realloc函数以及指针来实现顺序表的动态扩容。

关于size的理解:

  • base[size] 可以直接访问到数组最后一个元素的下一个位置。
  • size-1 是数组最后一个元素的下标

相关接口实现

数据结构的功能无非就是增、删、查、改以及遍历,接口如下:

// 数据结构管理的需求   增删查改

void SLInit(SeqList* ps);	//初始化
void SLDestroy(SeqList* ps);	//销毁
void checkCapacity(SeqList* ps);	//检查容量与扩容   将扩容抽象成一个函数

void SLPushBack(SeqList* ps, SLDataType data);	// 尾插与尾删
void SLPopBack(SeqList* ps);

void SLPushFront(SeqList* ps, SLDataType data);	// 头插与头删
void SLPushFront(SeqList* ps, SLDataType data);

void SLInsert(SeqList* ps, int pos, SLDataType data);	//在指定位置(给定数组下标)插入删除元素
void SLErase(SeqList* ps, int pos);

int FindSL(SeqList* ps, SLDataType ele);	//查找  顺序遍历暴力查找法

//打印顺序表
void SLPrint(SeqList* ps);

源文件实现

#include "SeqList.h"

//传入assert的值为真时, 通过   为假  报错, 并在终端中显示错误所在行

初始化

  • 断言结构体指针不能为空
  • 开辟空间,将空间地址赋值给ps->base
  • 判断是否开辟成功
  • 赋初值
    • 数组数据初始个数为 0
    • 数组容量初始化为 INIT_CAPACITY
void SLInit(SeqList* ps) {
	assert(ps);
	ps->base = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
	if (ps->base == NULL) {
		perror("malloc failed");
		return;
	}
	ps->size = 0;
	ps->capacity = INIT_CAPACITY;
}

销毁

  • 断言指针为非空
  • 释放结构体指针指向的堆区数组
  • 将指针置空, 防止悬空指针的问题
  • 将当前大小和总容积都设置为0
void SLDestroy(SeqList* ps) {
	assert(ps);
	free(ps->base);
	ps->base = NULL;
	ps->capacity = ps->size = 0;
	
}

扩容

一个一个插的时候,会到size == capacity的临界点, 因此只需检查临界点

  • 断言指针非空
  • 判断该数组是否满了(ps->size == ps->capacity)
  • 满了之后需要扩容, 使用realloc函数 SLDataType* newSpace = (SLDataType*)realloc(ps->base, sizeof(SLDataType) * ps->capacity * 2)
    • C++的STL中,扩容一般是扩容至当前容量的2倍, realloc函数(第二个参数为新空间的总大小,而不是要增加的大小, 单位是字节)扩容会有两种情况
      • 原地扩容, 在原空间的基础上增加容量
      • 异地扩容, 开辟一块新的大小的空间,将原数据拷贝至新的空间中,同时释放原空间。
      • 为防止异地扩容, 需要接受新空间的地址,将其赋值给指向原空间的指针。
  • 最后 ps->capacity *= 2
void checkCapacity(SeqList* ps) {
	assert(ps);
	//扩容
	// 一个一个插的时候,会到size == capacity的临界点, 因此只需检查临界点
	if (ps->size == ps->capacity) {
		//将容量扩至原来的两倍
		SLDataType* newSpace = (SLDataType*)realloc(ps->base, sizeof(SLDataType) * ps->capacity * 2);
		if (newSpace == NULL) {
			perror("realloc failed");
			return;
		}
		// realloc 函数 扩容会有两种情况  一种是开辟一个新的空间   另一种是在原空间的基础上扩容
		ps->base = newSpace;
		ps->capacity *= 2;
	}
}

尾插

  • 断言指针非空
  • 检查是否需要扩容
  • 插入
    • ps->base[size++] = data
  • 也可以调用后面的在任意位置插入的函数, 通过调整参数实现尾插
// 尾插与尾删
void SLPushBack(SeqList* ps, SLDataType data) {
	assert(ps);

	checkCapacity(ps);

	//插入数据
	ps->base[ps->size++] = data;

	//可以用任意位置插入代替尾插
	//SLInsert(ps, ps->size, data);
}

尾删

  • 删除前需要确保数组的size > 0
  • 删除,其实只需要将 size-- 即可, 后续再写入时直接将原数据覆盖
void SLPopBack(SeqList* ps) {
	//size的值为 0 时, 就不能再缩小了
	//暴力检查
	assert(ps->size > 0);
	//温柔的检查
	if (ps->size == 0)
		return;

	ps->size--;

	//SLErase(ps, ps->size - 1);
}

头插

插入N个数据的时间复杂度为O(N)

  • 断言指针非空
  • 头插入需要将数据一个一个挪动,为防止挪动时数据覆盖, 需要从后往前挪动 end = size -1, size-1 是数组中最后一个元素的下标
  • 插入前需要检查顺序表是否为满,如果满了需要扩容
  • 依次向后挪动
while (end >= 0) {
		ps->base[end + 1] = ps->base[end];
		end--;
	}
  • 挪动结束后赋值, 使size–
void SLPushFront(SeqList* ps, SLDataType data){
	assert(ps);
	int end = ps->size - 1;
	checkCapacity(ps);	//检查顺序表是否为满的
	while (end >= 0) {
		ps->base[end + 1] = ps->base[end];
		end--;
	}
	ps->base[0] = data;
	ps->size++;
}

头删

  • 断言指针非空
  • 断言数组的大小大于0
  • 将下标为 1 ~ size 的数据一次向左移动, 覆盖即可
  • size–
void SLPopFront(SeqList* ps, SLDataType data) {
	assert(ps);
	assert(ps->size > 0);

	int begin = 1;
	while (begin < ps->size) {
		ps->base[begin - 1] = ps->base[begin];
		begin++;
	}
	ps->size--;
}

在任意位置插入

  • 和头插的思路一样
    • 头插的停止位置是在base[0]
    • 任意位置插入的停止位置是在base[pos]
  • 断言指针非空以及 pos >= 0 && pos <= ps->size
void SLInsert(SeqList* ps, int pos, SLDataType data) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	checkCapacity(ps);

	int end = ps->size - 1;
	while (end >= pos) {
		ps->base[end + 1] = ps->base[end];
		end--;
	}
	ps->base[pos] = data;
	ps->size++;
}

删除任意位置的元素

  • 和头删法类似
    • 头删法的起始位置在base[0]
    • 任意位置删除的起始位置在base[pos+1]
  • 将右侧的元素向左移动
void SLErase(SeqList* ps, int pos) {
	assert(ps);
	assert(pos >= 0 && pos <= ps->size - 1);

	int begin = pos + 1;
	while (begin <= ps->size) {
		ps->base[begin - 1] = ps->base[begin];
		begin++;
	}
	ps->size--;
	
}

查找顺序表(暴力查找)

int FindSL(SeqList* ps, SLDataType ele) {
	assert(ps);
	for (int i = 0; i < ps->size; i++) {
		if (ps->base[i] == ele)
			return i;
	}
	return -1;
}

遍历顺序表

void SLPrint(SeqList* ps) {
	assert(ps && ps->base);
	for (int i = 0; i < ps->size; ++i) {
		printf("%d ", ps->base[i]);
	}
	printf("\n");
}

顺序表的劣势

  1. 中间/头部的插入删除, 时间复杂度为O(N)
  2. 增容需要申请新空间, 拷贝数据, 释放旧空间。会有不小的消耗。
  3. 增容一般是两倍的增长, 势必会有一定的空间浪费。
  4. 当存储的数据量过大时, relloc 函数扩容基本上都是异地扩容, 时间开销很大。

你可能感兴趣的:(《数据结构与算法》,数据结构,c语言)