文章目录
速通数据结构与算法系列
1 速通数据结构与算法第一站 复杂度 http://t.csdnimg.cn/sxEGF
感谢佬们支持!
目录
系列文章目录
- 前言
- 一、顺序表
- 0 结构体
- 1 接口声明
- 2 初始化和销毁
- 3 扩容函数
- 4 打印和判空
- 5 尾插
- 6 尾删
- 7 头插
- 8 头删
- 9 查找
- 10insert (指定位置下一个插入)
- 11erase (删除指定位置)
- 12完整代码
- 二、顺序表OJ
- 1 移除元素
- 2 删除有序数组中的重复项
- 3 合并两个有序数组
- 总结
前言
我们要接触到的第一种数据结构类型是线性表,线性表的定义是n个具有相同特性的数据元素的有限序列。常见的有:顺序表,链表,栈,队列,字符串……
线性表的逻辑结构是线性的,即一条连续的直线,但是其物理结构不一定是连续的,在物理上一般为数组和链式。
在这里我们要先区分一个概念:逻辑结构和物理结构
逻辑结构是一个抽象的结构,是我们研究某种东西时为了便于理解的一种归纳的思想
而物理结构则是真实的存储结构
举个例子来看,磁盘是什么结构?我们拿到一个磁盘会发现它是扇形的,他存储单元是一个个扇区,倘若我们把这些扇形一个个拉直,最后得到一条线,所以它的逻辑结构就是线性表
--》
一、顺序表
顺序表是用一段物理连续存储单元依次存储数据元素的线性结构,一般采用数组存储,利用数组来完成增删查改操作。
我们来浅浅实现一波,先创3个文件:test.c sqlist.c sqlist.h
在sqlist.h下……
为了达到C语言版的所谓泛型,模板,我们设一个宏
typedef int SLDataType;
我们先来写它的结构体
结构体
typedef struct sqlist
{
SLDataType* a;
//数据个数
int size;
//容量
int capacity;
}SL;
注意:在STL的实现中,vector的结构体成员(成员变量)有3个,int* start,int* end,int* endofstorage三个指针(迭代器,此处先暂且认为是int*类型),对应关系为
start=_a; finish=_a+_size; endofstorage=_a+capacity;
这样设计的好处就是,当我们swap(vector1,vector2)时;成本将非常低,因为只需交换3个指针而已。
为了方便我们之后STL的学习,我们接口的命名风格都按STL的来
我们在seqlist.h中包一下需要用到的头文件,
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
接口声明
并声明如下接口
//打印函数
void SeqListPrint(SL* ps);
//扩容函数
void BuyListNode(SL* ps);
//初始化
void SeqListInit(SL* ps);
//销毁
void SeqListDestroy(SL* ps);
//判空
bool SeqListCheckCapacity(SL* ps);
//尾插
void SeqListPushBack(SL* ps, SLDataType x);
//尾删
void SeqListPopBack(SL* ps);
//头插
void SeqListPushFront(SL* ps, SLDataType x);
//头删
void SeqListPopFront(SL* ps);
//查找 找到了返回下标,没找到返回-1
int SeqListFind(SL* ps, SLDataType x);
//指定pos位置后插
void SeqListInsert(SL* ps, SLDataType x, int pos);
//指定pos位置删
void SeqListErase(SL* ps, int pos);
初始化和销毁
考虑到动态内存泄漏的问题,我们先来实现初始化及其销毁函数
要做的事很简单,初始化时指针置空,size和capacity给个0即可
(其中要注意判空ps,因为我们不知道不知道传入的ps是否为空)
//初始化
void SeqListInit(SL* ps)
{
assert(ps);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
//销毁
void SeqListDestroy(SL* ps)
{
assert(ps);
free(ps->a);
ps->a=NULL;
ps->capacity = ps->size = 0;
}
扩容函数
为了便于调用,我们封装出一个扩容函数来
由于刚开始容量为0,我们第一次给4,之后进行二倍扩容
(二倍扩容采用的是SGI STL的扩容策略)
//扩容函数
void BuyListNode(SL* ps)
{
assert(ps);
//0就给4,不是就给capacity2倍
int newcapacity = (ps->capacity == 0) ? 4 : (ps->capacity * 2);
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
//扩容失败
if (NULL == tmp)
{
printf("realloc fail");
exit(-1);
}
//扩容成功
ps->a = tmp;
ps->capacity = newcapacity;
}
我们简单地来测试一下
SL sl;
SeqListInit(&sl);
SeqListDestroy(&sl);
(确实挺不错的)
打印和判空
下来我们再写个简单的打印和判空
打印就简单了,遍历打印;判空的逻辑就是size是否等于0
//打印函数
void SeqListPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
}
//判空
bool SeqListCheckCapacity(SL* ps)
{
assert(ps);
return ps->size == 0;
}
接下来就该是顺序表核心的插入删除操作了
尾插
我们先来进行尾插,尾插简单,在数组的最后插就行了,顺便更新一下size
注意:在插之前,我们先要考虑是否需要扩容的问题
void SeqListPushBack(SL* ps, SLDataType x)
{
assert(ps);
//考虑扩容
if (ps->capacity == ps->size)
{
BuyListNode(ps);
}
//插
ps->a[ps->size] = x;
ps->size++;
}
另外 我们知道,此时的扩容并非在数组后面直接扩容,而是再开一块空间,并将原来的数据拷过来,并释放原空间,在STL的学习中会有所谓迭代器失效的问题,因为之前的空间没了
所以指向之前空间的迭代器就会失效,我们不能用了,但是在C语言中,这种场景则不太容易碰到。
尾删
尾删就更简单了,我们直接--size即可,但是我们需要断言一下size>0
//尾删
void SeqListPopBack(SL* ps)
{
assert(ps);
//断言一下size,size要大于0
assert(ps->size > 0);
ps->size--;
}
显然,我们发现,尾插,尾删的时间复杂度都是O(1)的。
我们来测试一下尾插和尾删
SL sl;
SeqListInit(&sl);
SeqListPushBack(&sl,1);
SeqListPushBack(&sl, 2);
SeqListPushBack(&sl, 3);
SeqListPushBack(&sl, 4);
SeqListPrint(&sl);
printf("\n");
SeqListPopBack(&sl);
SeqListPushBack(&sl, 5);
SeqListPrint(&sl);
(没问题)
下来就是有点麻烦的头插和头删了
头插
头插的逻辑是这样的,我们需要讲数组往后挪一格,再在第一个位置做插入
-》
其中这个挪就有意思了,是从前往后挪还是从后往前挪呢?
当然是从后往前挪,不然会覆盖数据
//头插
void SeqListPushFront(SL* ps, SLDataType x)
{
assert(ps);
//考虑扩容
if (ps->capacity == ps->size)
{
BuyListNode(ps);
}
//插
//从后往前挪数据
int len = ps->size-1;
while (len>=0)
{
ps->a[len+1] = ps->a[len];
--len;
}
ps->a[0] = x;
ps->size++;
}
头删
头删也需要挪动数据,不过和头插恰好相反,他需要从前往后开始挪
//头删
void SeqListPopFront(SL* ps)
{
assert(ps);
//挪数据
int len = 0;
while (len <= ps->size - 1)
{
ps->a[len] = ps->a[len + 1];
++len;
}
ps->size--;
}
显然,头插和头删的时间复杂度都是O(n),所以精明的STL并没有单独提供头插头删操作
我们再来简单的测试一下
SL s2;
SeqListInit(&s2);
SeqListPushFront(&s2, 1);
SeqListPushFront(&s2, 2);
SeqListPushFront(&s2, 3);
SeqListPushFront(&s2, 4);
SeqListPrint(&s2);
printf("\n");
SeqListPopFront(&s2);
(同样没有任何问题)
查找
下来我们写一下查找
查找的逻辑很简单,遍历数组,找到了返回下标,没找到返回-1
//查找 找到了返回下标,没找到返回-1
int SeqListFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size ; ++i)
{
if (x == ps->a[i])
{
return i;
}
}
return -1;
}
查找是为之后的两个函数服务的:insert 和erase
因为在实际运用时,我们一般都是先找到某个元素,再插入/删除
我们写的是删除指定位置和在指定位置的下一个插入
(STL提供的是删除指定位置和指定位置前一个插入)
写过了之前的头删,写这个insert就简单了
insert
我们依然需要从后往前插的方式,而且在之前需要判断一下pos的合法性
//指定pos位置后插
void SeqListInsert(SL* ps, SLDataType x, int pos)
{
assert(pos>=0&&pos <= ps->size);
//考虑扩容
if (ps->capacity == ps->size)
{
BuyListNode(ps);
}
//挪动数据
int len = ps->size;
while (len-pos+1)
{
ps->a[len + 1] = ps->a[len];
--len;
}
ps->a[pos + 1] = x;
++ps->size;
}
erase同理
erase
//指定pos位置删
void SeqListErase(SL* ps, int pos)
{
assert(pos>=0&&pos <= ps->size);
//pos+1往前挪
int len = ps->size-1;
while (len-pos)
{
ps->a[len-1] = ps->a[len];
len--;
}
--ps->size;
}
我们再来简单的测试一下
SL s3;
SeqListInit(&s3);
SeqListPushBack(&s3, 1);
SeqListPushBack(&s3, 2);
SeqListPushBack(&s3, 3);
SeqListPushBack(&s3, 4);
SeqListPushBack(&s3, 5);
SeqListPrint(&s3);
printf("\n");
int pos=SeqListFind(&s3,4);
printf("%d\n", pos);
//SeqListInsert(&s3, 7, pos);
SeqListPrint(&s3);
printf("\n");
SeqListInsert(&s3, 8, pos);
SeqListInsert(&s3, 9, pos);
SeqListErase(&s3, pos);
SeqListPrint(&s3);
12 完整代码
sqlist.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int SLDataType;
typedef struct sqlist
{
SLDataType* a;
//数据个数
int size;
//容量
int capacity;
}SL;
//打印函数
void SeqListPrint(SL* ps);
//扩容函数
void BuyListNode(SL* ps);
//初始化
void SeqListInit(SL* ps);
//销毁
void SeqListDestroy(SL* ps);
//判空
bool SeqListCheckCapacity(SL* ps);
//尾插
void SeqListPushBack(SL* ps, SLDataType x);
//尾删
void SeqListPopBack(SL* ps);
//头插
void SeqListPushFront(SL* ps, SLDataType x);
//头删
void SeqListPopFront(SL* ps);
//查找 找到了返回下标,没找到返回-1
int SeqListFind(SL* ps, SLDataType x);
//指定pos位置后插
void SeqListInsert(SL* ps, SLDataType x, int pos);
//指定pos位置删
void SeqListErase(SL* ps, int pos);
sqlist.c
#include "seqlist.h"
//初始化
void SeqListInit(SL* ps)
{
assert(ps);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
//销毁
void SeqListDestroy(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
//扩容函数
void BuyListNode(SL* ps)
{
assert(ps);
//0就给4,不是就给capacity2倍
int newcapacity = (ps->capacity == 0) ? 4 : (ps->capacity * 2);
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
//扩容失败
if (NULL == tmp)
{
printf("realloc fail");
exit(-1);
}
//扩容成功
ps->a = tmp;
ps->capacity = newcapacity;
}
//打印函数
void SeqListPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
}
void SeqListPushBack(SL* ps, SLDataType x)
{
assert(ps);
//考虑扩容
if (ps->capacity == ps->size)
{
BuyListNode(ps);
}
//插
ps->a[ps->size] = x;
ps->size++;
}
//尾删
void SeqListPopBack(SL* ps)
{
assert(ps);
//断言一下size,size要大于0
assert(ps->size > 0);
ps->size--;
}
//头插
void SeqListPushFront(SL* ps, SLDataType x)
{
assert(ps);
//考虑扩容
if (ps->capacity == ps->size)
{
BuyListNode(ps);
}
//插
//从后往前挪数据
int len = ps->size - 1;
while (len >= 0)
{
ps->a[len + 1] = ps->a[len];
--len;
}
ps->a[0] = x;
ps->size++;
}
//头删
void SeqListPopFront(SL* ps)
{
assert(ps);
//挪数据
int len = 0;
while (len <= ps->size - 1)
{
ps->a[len] = ps->a[len + 1];
++len;
}
ps->size--;
}
//查找 找到了返回下标,没找到返回-1
int SeqListFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; ++i)
{
if (x == ps->a[i])
{
return i;
}
}
return -1;
}
//指定pos位置后插
void SeqListInsert(SL* ps, SLDataType x, int pos)
{
assert(pos >= 0 && pos <= ps->size);
//考虑扩容
if (ps->capacity == ps->size)
{
BuyListNode(ps);
}
//挪动数据
int len = ps->size;
printf("%d\n", len);
while (len - pos + 1)
{
ps->a[len + 1] = ps->a[len];
--len;
}
ps->a[pos + 1] = x;
++ps->size;
}
//指定pos位置删
void SeqListErase(SL* ps, int pos)
{
assert(pos >= 0 && pos <= ps->size);
//pos+1往前挪
int len = ps->size - 1;
while (len - pos)
{
ps->a[len - 1] = ps->a[len];
len--;
}
--ps->size;
}
test.c(测试代码)
#include "seqlist.h"
void test1()
{
SL sl;
SeqListInit(&sl);
SeqListPushBack(&sl,1);
SeqListPushBack(&sl, 2);
SeqListPushBack(&sl, 3);
SeqListPushBack(&sl, 4);
SeqListPrint(&sl);
printf("\n");
SeqListPopBack(&sl);
SeqListPushBack(&sl, 5);
SeqListPrint(&sl);
SeqListDestroy(&sl);
}
void test2()
{
SL s2;
SeqListInit(&s2);
SeqListPushFront(&s2, 1);
SeqListPushFront(&s2, 2);
SeqListPushFront(&s2, 3);
SeqListPushFront(&s2, 4);
SeqListPrint(&s2);
printf("\n");
SeqListPopFront(&s2);
SeqListPrint(&s2);
SeqListDestroy(&s2);
}
void test3()
{
SL s3;
SeqListInit(&s3);
SeqListPushBack(&s3, 1);
SeqListPushBack(&s3, 2);
SeqListPushBack(&s3, 3);
SeqListPushBack(&s3, 4);
SeqListPushBack(&s3, 5);
SeqListPrint(&s3);
printf("\n");
int pos=SeqListFind(&s3,4);
SeqListInsert(&s3, 7, pos);
SeqListPrint(&s3);
printf("\n");
SeqListInsert(&s3, 8, pos);
SeqListInsert(&s3, 9, pos);
SeqListErase(&s3, pos);
SeqListPrint(&s3);
}
int main()
{
test3();
return 0;
}
二、顺序表OJ
学完了顺序表的实现,我们可以写一些有关顺序表的OJ题
1 移除元素
题目链接: . - 力扣(LeetCode)
这个题就是简单的双指针,我们可以定义两个变量, src用来遍历数组,dst用来存放不是val的值,并标记移除元素后的新长度
由于它要的是新长度,所以我们不用真的删除那些元素。
int removeElement(int* nums, int numsSize, int val)
{
int src = 0;
int dst = 0;
while (src < numsSize)
{
if (nums[src] != val)
{
nums[dst] = nums[src];
++dst;
++src;
}
else
{
++src;
}
}
return dst;
}
关于双指针这类算法,后续我们会进行系统的学习和刷题哦~
2 删除有序数组中的重复项
题目链接: . - 力扣(LeetCode)
由于要求原地删除,所以我们不能再搞一个数组,把不重复的元素push_back进去
这道题我们依旧采用双指针来做
但是我们需要三个指针,两个cur,next用来遍历,一个dst用来记录不重复元素的个数
我们的逻辑是这样,如果cur和next指向值相同的数就让next++,指向值不相同的数时
用dst下标记录cur指向的值,再让dst++,让cur去next的位置,再让next++
如图所示
-》
-》
(此时第一个下标就记录了0这个元素)
但是还有个问题:到了要结束的时候,此时cur,next指向的元素不同,next++就跳出了循环
所以最后一个元素我们在循环结束后要再补充至dst下标上
由此可得代码如下
int removeDuplicates(int* nums, int numsSize)
{
int cur = 0;
int next = 1;
int dst = 0;
//考虑为空
if (numsSize == 0)
{
return 0;
}
while (next < numsSize)
{
if (nums[next] == nums[prev])
{
next++;
}
else
{
nums[dst++] = nums[prev];
prev = next;
next++;
}
}
nums[dst++]=nums[prev];
return dst;
}
3 合并两个有序数组
题目链接 : . - 力扣(LeetCode)
显然,我们的任务是把nums2的拷入nums1中,
两个数组一个给一个指针int i=0,int j=0,遍历数组找小的;再标记一个dst指针,将较小者插入nums[dst++];
当两个数组有一个走完了另一个数组的元素再依次拷入剩下的位置即可。
啊但是!
当你画了图模拟一遍之后就会发现,如果我们从前往后开始会有覆盖的可能
如图,当nums2的2和nums1的3比较时,由于2<3,显然2要到3的位置,所以这个3就会被覆盖
那怎么办呢?
以我们刚才头插头删的经验来看,从前往后会覆盖,那我们就从后往前
所以我的指针变为int end1=m-1,int end2=n-1,int end=m+n-1。比较的时候也是比较大的
所以代码如下
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
int end1=m-1;
int end2=n-1;
int end=m+n-1;
while (end1>=0 && end2>=0)
{
if (nums1[end1] > nums2[end2])
{
nums1[end--] = nums1[end1--];
}
else
{
nums1[end--] = nums2[end2--];
}
}
//有一个走完了
while (end2>=0)
{
nums1[end--] = nums2[end2--];
}
}
总结
做总结,这篇博客我们学习了了顺序表,顺序表的优势是由于有下标,可以随机访问,尾插尾删效率高。下篇博客我们要学习可能有些些许难得链表,如果你的指针有些遗忘了,建议去复习一下指针哦~
水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。