数据结构:顺序表

一.概念及结构

1.概念
顺序表是用一段 物理地址连续 的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表可分为静态和动态两种:
(1)静态:

 静态顺序表弊端较大,如果空间过小则不足,空间过大则浪费。

(2)动态

对数组进行扩容处理,和动态内存管理类似。

2.区分:结构体和柔性数组 

结构体:a指向这块空间

柔性数组:size capacity在同一空间内,后续数据在后面添加

3.结构体传参

结构体传参要传地址,否则实参不会发生变化

二.顺序表接口实现

1.头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLDataType;//定义整型结构体
#define INIT_CAPACITY 4

// 动态顺序表 -- 按需申请
typedef struct SeqList
{
	SLDataType* a;
	int size;     // 有效数据个数
	int capacity; // 空间容量
}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);//寻找
2.初始化结构体
#include "SeqList.h"//包含头文件,定义参数为4
//初始化
void SLInit(SL* ps)
{
	ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);//开辟一段4字节的空间
	if (ps->a == NULL)//判断是否开辟成功
	{
		perror("malloc fail");
		return;
	}
	ps->size = 0;
	ps->capacity = INIT_CAPACITY;
}
 3.尾插
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
	if (ps->capacity = ps->size)//判断是否要进行扩容操作
	{
		SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);//扩容
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity *= 2;
	}
	//ps->a[ps->size] = x;
	//ps->size++;
	ps->a[ps->size++] = x;//上写两种写法都可以,下面这个写法更简洁
    //就是ps先指向size也就是最后一个位置的下一位,然后size++
}
小bug

这里有一点需要注意

 上面那个图会出现debug,因为他的扩容是存在问题的,这里ps->capacity前面定义为4字节,乘2变为8字节,但是我们需要的是8个SLDataType(整形)位共计32字节,这里就产生了越界。

我们怎么去发现这个问题的:

free的断点bug问题

调试的过程中,我们发现free处出现了断点(见二.10),free处出现断点就有两种情况:1.出现野指针;2.出现越界情况,而多半是第二种情况,所以我们发现了是越界情况产生的。

4.尾删
void SLPopback(SL* ps)
{
	//ps->a[ps->size-1]=0;
	ps->a[ps->size--];
}

 这里屏蔽掉的那行代码可以选择不要,因为顺序表本来就是记录连续的size个数据,有效数据只有size个,不必规0。

 测试

这里出现了一个debug,这是由于删的太多导致的,size为负,就出现问题了,因此我们要对尾删做出修改。

暴力检查和温柔检查
void SLPopback(SL* ps)
{
	//暴力检查
	assert(ps->size > 0);
	//ps->a[ps->size-1]=0;
	ps->a[ps->size--];
}

 使用断言检查,一旦出现size<=0的情况,立即终止报错。

void SLPopback(SL* ps)
{
	//温柔检查
	if (ps->size == 0)
		return;
	//ps->a[ps->size-1]=0;
	ps->a[ps->size--];
}

ps->size为0时自动停止,不会报错

5.头插

引入checkcapacity函数,节省代码空间(就是为了避免开辟空间失败)

下面是头插部分

void SLPushfront(SL* ps,SLDataType x)
{
	assert(ps);
	//SLCheckCapacity(ps);
	SLDataType end = ps->size - 1;//指向最后一个位置
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];//把end位置给给下一个位置,腾出来位置给开头
		end--;
	}
	ps->a[0] = x;
	ps->size--;
}
6.头删
void SLPopfront(SL* ps)
{
	assert(ps);
	assert(ps->size > 0);
	SLDataType begin = 1;
	while (begin < ps->size)//后面覆盖前面的,一点一点覆盖
	{
		ps->a[begin - 1] = ps->a[begin];
		++begin;//往后移一个位置
	}
	ps->size--;//减少一个元素size
}
测试

 7.插入数据
void SLInsert(SL* ps, int pos, SLDataType x)//在pos的下一个位置插入数据
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	SLCheckCapacity(ps);
	
	int end = ps->size - 1;//走到最后一个位置
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	
	ps->a[pos] = x;
	ps->size++;
}

插和尾插其实也可以通过上述的方法解决:

SLInsert(ps, 0, x);  //头插
SLInsert(ps, ps->size, x);   //尾插
8.删除数据
void SLErase(SL* ps, int pos)//删除pos后面的元素
{
	assert(ps);
	assert(pos >= 0 && pos < ps -> size);
	int begin = pos + 1;
	while (begin < ps->size)
	{
		ps->a[begin-1] = ps->a[begin];//pos元素后面的元素是被覆盖的,pos是从1开始到第pos个位置
		++begin;
	}
	ps->size--;
}
9.查找数据
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; ++i)
	{
		if (ps->a[i] == x)
			return i;
	}
	return -1;
}
10.释放空间
void SLDestroy(SL* ps)
{
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->size = 0;
}
11.复杂度

尾插/尾删N个数据的时间复杂度:O(N)

头插/头删N个数据的时间复杂的:O(N^2)

可以看出顺序表更适合尾插尾删

三.realloc详解

1.基本概念
realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc ( void* ptr , size_t size );
ptr 是要调整的内存地址  size 调整之后新大小  返回值为调整之后的内存起始位置。
2.realloc的原地扩容和异地扩容
(1).原地扩容

 开辟对应的空间没有被占用,则进行原地扩容。

地址一样,为原地扩容

(2).异地扩容

 开辟对应的空间被占用,则另开辟一块更大的没有被占用的空间,将原数据拷贝到新空间,并释放原先的空间。

 地址不同,为异地扩容。

3.realloc的扩容原理

 这里很明显出现了越界访问,显示系统崩溃。

当我们通过ptr3将代码的realloc范围控制到5后,再次realloc到20,发现地址不变,说明是在同一个地址上,也就是说ptr3的操作并没有将空间销毁,而是将数据回收。 

 通过上下两图的对比可以发现,realloc是只扩不缩的,ptr3那行代码只是将多余空间的使用权收回,而并非销毁,这样可以保证再扩的时候还能原地扩容。

 四.OJ实战

1.移除元素

力扣icon-default.png?t=N7T8https://leetcode.cn/problems/remove-element/

 这里我们来画图解决这个问题:

 双向指针解法,将该数组存到两个指针src和dst内,逢2,src就往下走一个,dst不动,直到src遍历整个数组。

int removeElement(int* nums, int numsSize, int val){
    int src=0,dst=0;
    while(src<numsSize)
    {
        if(nums[src]!=val)
        {
            nums[dst++]=nums[src++];
        }
        else
        {
            src++;
        }
    }
    return dst;
}
2.删除有序数组中的重复项

力扣icon-default.png?t=N7T8https://leetcode.cn/problems/remove-duplicates-from-sorted-array/

 数据重复,则跳过该数据;数据不重复,则在下一个位置录入该数据。

int removeDuplicates(int* nums, int numsSize){
    int hgs=1,sct=0;
    while(hgs<numsSize)
    {
        if(nums[hgs]==nums[sct])
        {
            hgs++;
        }
        else if(nums[hgs]!=nums[sct])
        {
            nums[++sct]=nums[hgs++];
        }
    }
    return sct+1;
}
3.合并两个有序数组

力扣icon-default.png?t=N7T8https://leetcode.cn/problems/merge-sorted-array/

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
    int end1=m-1,end2=n-1;
    int hgs=m+n-1;
    while(end1>=0&&end2>=0)
    {
        if(nums1[end1]>nums2[end2])
        {
            nums1[hgs--]=nums1[end1--];
        }
        else
        {
            nums1[hgs--]=nums2[end2--];
        }
    }
    while(end2>=0)
    {
        nums1[hgs--]=nums2[end2--];
    }
}

五.顺序表的缺陷

1.中间头部插入删除数据,需要挪动数据,效率低下;

2.空间不够,扩容。扩容会有一定的消耗,还可能会有一定的空间浪费。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值