【数据结构】顺序表的学习

简介: 【数据结构】顺序表的学习

前言:在之前我们学习了C语言的各种各样的语法,因此我们今天开始学习数据结构这一个模块,因此我们就从第一个部分来开始学习"顺序表"。

d6dc0126edd141a985d72de501ef756b.jpg


顺序表

  1. 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
  2. 顺序表一般可以分为两个部分,静态顺序表(使用定长数组存储元素)和动态顺序表(使用动态开辟的数组存储)。
  3. 静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。

动态顺序表

首先我们创建一个结构体,去实现数据的动态存储。

代码如下:

typedef int SLDataype;//定义整型,方便后面进行数据的更改

typedef struct SeqList //动态顺序表的开辟
{
  SLDataype* a;
  int size; //记录多少个有效数据
  int capacity; //记录空间有多大
}SL;

接下来我们就实现一个个接口来实现顺序表的基本功能

顺序表的初始化

顺序表的初始化十分简单,只需要将开辟的指针置为空指针,有效数据清零,空间容量清零即可。

代码如下:

void SLInit(SL* ps1)//顺序表的初始化
{
  assert(ps1);
  ps1->a = NULL;//将指针置为空指针
  ps1->size = 0;//数据清零
  ps1->capacity = 0;//容量清零
}


顺序表的销毁

同理,顺序表的销毁,只需要判断所开辟的空间是否已经是被释放了,和被置为空指针了,如果不是,就把所开辟的空间释放和数据清零即可。

代码如下:

void SLDestory(SL* ps1)//顺序表的销毁
{
  if (ps1->a != NULL)//判断该指针是否为空指针
  {
    free(ps1->a);//释放该指针所开辟的空间
    ps1->a = NULL;//置为空指针
    ps1->size = 0;//数据清零
    ps1->capacity = 0;//容量清0
  }
}

顺序表的空间容量检查

我们想要实现在所开辟的空间中,增加数据,就不可避免的需要去检查所开辟的空间的容量是否充足,如果不充足,我们就需要去增容,那该增加多少呢?增加太多,会导致空间的浪费,太少又会导致不断的扩容,这是一个双向的问题,因此这也没有什么十分固定的答案,通常来讲,一般增容一倍即可。


代码思路:在检查空间的时候,我们应该考虑传过来的指针是否是空指针,即是否是初始化后的顺序表,但realloc函数是可以对空指针进行扩容的,但是我们为了防止扩容失败,应该开辟一个新的结构体指针来接收这块空间,在开辟成功后重新覆盖回去。

代码实现:

void SLCheckCapacity(SL* ps1)//检查空间
{
  if (ps1->size == ps1->capacity)
  //判断记录的数据个数,与空间容量释放相同,相同的时候即空间满了,需要增容
  {
    int newCapacity = ps1->capacity == 0 ? 4 : ps1->capacity * 2;
    //判断开辟的空间是否是初始化的空间,如果是就让他容量为4,如果不是就开辟当前空间的两倍
    SLDataype* tmp = (SLDataype*)realloc(ps1->a, sizeof(SLDataype) * newCapacity);
    //创建一个新的结构体指针,在他的后面扩容,放在开辟失败后原本的数据也不见了
    //realloc 可以对空指针进行扩容
    if (tmp == NULL)
    {
      perror("realloc fail");
      return;
    }
    ps1->a = tmp;//将开辟后面的指针传给原本的地址
    ps1->capacity = newCapacity;//更新容量
  }
}

顺序表的打印

void SLPrint(SL* ps1)
{
  for (int i = 0; i < ps1->size; i++)//变量开辟的数据
  {
    printf("%d ", ps1->a[i]);//直接打印
  }
  printf("\n");
}

顺序表的尾增

代码思路:因为前面我们提到了size是有效数据的个数,也是指向的数组的最后一个元素的后一个的位置,因此只需要在此位置赋值即可,然后再让size往后偏移一位。

代码实现:

void SLPushBack(SL* ps1, SLDataype x)//尾增
{
  SLCheckCapacity(ps1);//检查空间容量
  ps1->a[ps1->size] = x;
  //因size指向的是数组最后一个元素的后一个元素,因此只需要直接在此位置赋值即可
  ps1->size++;//size继续指向后一个元素
}

现在我们实现了四个小的接口,忙活了这么久,我们来见一下效果,好歹听个响是吧。

效果图:

void TestSL1()//测试函数
{
  SL s1;//创建动态顺序表
  SLInit(&s1);//初始化顺序表
  SLPushBack(&s1, 2);//尾增数据
  SLPushBack(&s1, 3);
  SLPushBack(&s1, 4);
  SLPrint(&s1);//打印数据
  SLPushBack(&s1, 5);
  SLPushBack(&s1, 6);
  SLPrint(&s1);//打印数据
  SLDestory(&s1);//销毁顺序表
}

int main()
{
  TestSL1();
  return 0;
}


顺序表的头增

代码实现思路:我们记录数据的最后一个位置为end,最后一个数据后面的一个数据位置为end+1,让end依次往后覆盖,然后end–直到end到起始位置即停止循环,具体思路如下图:

代码实现:

void SLPushFront(SL* ps1, SLDataype x)//头增
{
  SLCheckCapacity(ps1);//先检查数据

  //挪动数据
  int end = ps1->size - 1;//记录最后一个位置
  while (end >= 0)//end到起始位置时即覆盖完成
  {
    ps1->a[end + 1] = ps1->a[end];//将数据依次往后覆盖
    --end;
  }
  ps1->a[0] = x;//将数据赋值给起始位置就可以实现头增
  ps1->size++;//size++
}

当然了,不能白忙活,我们来测试一下这个函数,来看看效果如何

void TestSL2()
{
  SL s1;//创建动态顺序表
  SLInit(&s1);//初始化顺序表
  SLPushBack(&s1, 2);//尾增数据
  SLPushBack(&s1, 3);
  SLPushBack(&s1, 4);
  SLPrint(&s1);//打印尾增的数据
  printf("上面是尾增的数据\n");
  printf("------------------------------\n");
  printf("下面是头增的数据\n");
  SLPushFront(&s1, 20);//头增的数据
  SLPushFront(&s1, 10);
  SLPushFront(&s1, 0);
  SLPushFront(&s1,9);
  SLPrint(&s1);//打印头增的数据
  SLDestory(&s1);//销毁顺序表
}


int main()
{
  //TestSL1();
  TestSL2();
  return 0;
}


顺序表的头删

代码思路:我们记录一个起始位置即数组的第二个元素记录为begin,让他往前面一个数据begin-1覆盖,然后再放入循环中不断覆盖,当运行到size的位置的时候停止循环具体思路如下图:

代码如下:

void SLPopFront(SL* ps1)//头删
{
  assert(ps1->size > 0);//判断传过来是是不是为空数据
  int begin = 1;//将起始位置置为1
  while (begin < ps1->size)//再起始位置等于size时即覆盖完成
  {
    ps1->a[begin - 1] = ps1->a[begin];//后面覆盖前面
    ++begin;
  }
  ps1->size--;//size减减
}


当然,我们依然来听个响,听听声音,效果图如下:


void TestSL3()
{
  SL s1;//创建动态顺序表
  SLInit(&s1);//初始化顺序表
  SLPushBack(&s1, 2);//尾增数据
  SLPushBack(&s1, 3);
  SLPushBack(&s1, 4);
  SLPushFront(&s1, 20);//头增的数据
  SLPushFront(&s1, 10);
  SLPushFront(&s1, 0);
  SLPushFront(&s1, 9);
  SLPrint(&s1);//打印原本的数据
  printf("上面是原本的数据\n");
  printf("--------------------------------------\n");
  printf("下面是删除后的数据\n");
  SLPopFront(&s1);//头删数据
  SLPopFront(&s1);
  SLPopFront(&s1);
  SLPrint(&s1);//打印删除后的数据
  SLDestory(&s1);//销毁顺序表
}

int main()
{
  //TestSL1();
  //TestSL2();
  TestSL3();
  return 0;
}


顺序表的尾删

代码思路:我们可以通过前面的打印函数可以知道,我们是通过打印到size前面的一个下标来访问所有的元素,因此我们只需要让size往前面走一个,就可以把尾部的元素删除,可以看下图思路:

代码实现:

void SLPopBack(SL* ps1)//尾删
{
  //空
  if (ps1->size == 0)//判断删除的是否为空
  {
    return;
  }
  ps1->size--;//size--即可
}

我们依然来测试一下我们函数,看看效果是否成功实现,代码和效果图如下:

void TestSL4()
{
  SL s1;//创建动态顺序表
  SLInit(&s1);//初始化顺序表
  SLPushBack(&s1, 2);//尾增数据
  SLPushBack(&s1, 3);
  SLPushBack(&s1, 4);
  SLPushFront(&s1, 20);//头增的数据
  SLPushFront(&s1, 10);
  SLPushFront(&s1, 0);
  SLPushFront(&s1, 9);
  SLPrint(&s1);//打印原本的数据
  printf("上面是原本的数据\n");
  printf("--------------------------------------\n");
  printf("下面是删除后的数据\n");
  SLPopBack(&s1);//尾删数据
  SLPopBack(&s1);
  SLPrint(&s1);//打印删除后的数据
  SLDestory(&s1);//销毁顺序表
}

int main()
{
  //TestSL1();
  //TestSL2();
  //TestSL3();
  TestSL4();
  return 0;
}


顺序表的选择插入

代码思路:选择插入,即将一个值,任意插入到顺序表中的范围内,当然了口头总是不太好说清,我们对着下图来进行一一分析,我们可以记录一个end坐标记录数据的尾部,然后将值一一覆盖,pos为我们想要出入数据的位置,例如下图,当end走到pos的位置的时候,即可停止覆盖,这有点类似与我们前面的头删,将值pos之后的值全部往后挪动了一位,然后将最后重复的值被想插入的值覆盖即可。

代码实现:

void SLInsert(SL* ps1, int pos, SLDataype x)//插入数据
//pos是下标,size是数据个数,看作下标的话,他是最后一个数的下一个位置
{
  assert(ps1);//判断传来的值是否是空指针
  assert(pos >= 0 && pos <= ps1->size);//插入的值必须是再范围内(可以包括尾增)
  SLCheckCapacity(ps1);//检查空间
  // 挪动数据
  int end = ps1->size - 1;//找到尾坐标
  while (end >= pos)
  {
    ps1->a[end + 1] = ps1->a[end];//将值一一覆盖,类似于头删
    --end;
  }
  ps1->a[pos] = x;//赋值
  ps1->size++;
}

我们依然来看看函数的效果是否实现,测试代码和效果图如下:

void TestSL5()
{
  SL s1;//创建动态顺序表
  SLInit(&s1);//初始化顺序表
  SLPushBack(&s1, 2);//尾增数据
  SLPushBack(&s1, 3);
  SLPushBack(&s1, 4);
  SLPushFront(&s1, 20);//头增的数据
  SLPushFront(&s1, 10);
  SLPushFront(&s1, 0);
  SLPushFront(&s1, 9);
  SLPrint(&s1);//打印原本的数据
  printf("上面是原本的数据\n");
  printf("--------------------------------------\n");
  printf("下面是选择插入后的数据\n");
  SLInsert(&s1, 3, 99);
  SLInsert(&s1, 1, 2);
  SLPrint(&s1);//打印选择插入后的数据
  SLDestory(&s1);//销毁顺序表
}
int main()
{
  //TestSL1();
  //TestSL2();
  //TestSL3();
  //TestSL4();
  TestSL5();
  return 0;
}



顺序表的删除数据

代码思路:删除数据和选择插入的概念其实大差不差,首先也应当判断传过来的数是否是空指针,删除的数据在不在数据范围内,然后我们再通过图文来进行分析,如下图,先把删除的位置记录下来,当然了,和前面同理依次往前面覆盖,再打印的时候直接将size往前移一位即可把最后重复的值给去掉,达到顺序表的删除的功能。

函数代码:

void SLErase(SL* ps1, int pos)//删除数据
{
  assert(ps1);
  assert(pos >= 0 && pos < ps1->size);
  int begin = pos + 1;//记录删除数据后的位置
  while (begin < ps1->size)
  {
    ps1->a[begin - 1] = ps1->a[begin];//依次覆盖
    ++begin;
  }
  ps1->size--;
}

代码实现和函数实现效果图如下:

void TestSL6()
{
  SL s1;//创建动态顺序表
  SLInit(&s1);//初始化顺序表
  SLPushBack(&s1, 2);//尾增数据
  SLPushBack(&s1, 3);
  SLPushBack(&s1, 4);
  SLPushFront(&s1, 20);//头增的数据
  SLPushFront(&s1, 10);
  SLPushFront(&s1, 0);
  SLPushFront(&s1, 9);
  SLPrint(&s1);//打印原本的数据
  printf("上面是原本的数据\n");
  printf("--------------------------------------\n");
  printf("下面是选择插入后的数据\n");
  SLErase(&s1, 3);//删除数据
  SLErase(&s1, 1);//删除数据
  SLPrint(&s1);//打印选择插入后的数据
  SLDestory(&s1);//销毁顺序表

}

int main()
{
  //TestSL1();
  //TestSL2();
  //TestSL3();
  //TestSL4();
  //TestSL5();
  TestSL6();
  return 0;
}


顺序表数据的查找

代码思路:顺序表数据的查找就过于简单了,我们只需要遍历数据,看看是否有对应的值,有则返回该数的下标,没有则返回**-1**。代码如下:

int SLFind(SL* ps1, SLDataype x)//顺序表数据的查找
{
  assert(ps1);//判断传来的数据是否是空指针
  for (int i = 0; i < ps1->size; i++)//遍历数组
  {
    if (ps1->a[i] == x)
      return i;//找到即返回坐标i
  }
  return -1;//没找到返回-1
}

函数测试与效果图:

void TestSL7()
{
  SL s1;//创建动态顺序表
  SLInit(&s1);//初始化顺序表
  SLPushBack(&s1, 2);//尾增数据
  SLPushBack(&s1, 3);
  SLPushBack(&s1, 4);
  SLPushFront(&s1, 20);//头增的数据
  SLPushFront(&s1, 10);
  SLPushFront(&s1, 0);
  SLPushFront(&s1, 9);
  SLPrint(&s1);//打印原本的数据
  printf("上面是原本的数据\n");
  printf("--------------------------------------\n");
  printf("下面是选择插入后的数据\n");
  printf("该数的下标是:%d\n",SLFind(&s1, 2));
  SLDestory(&s1);//销毁顺序表
}
int main()
{
  //TestSL1();
  //TestSL2();
  //TestSL3();
  //TestSL4();
  //TestSL5();
  //TestSL6();
  TestSL7();
  return 0;
}

讲到这里,关于顺序表的基本内容实现全部讲完了,接下来我们来看一题目练习一下把!


练习

题目链接:

思路分析:看到这个题,大家都会想到一个很通俗易懂的方法,就是开辟一个辅助数组,将所有的数放进去,然后进去排序,当然我们今天是不讲这个方法的,因为这个方法的时间复杂度过于高。我们看到这个是一个非递减顺序,可以知道最大的数都放在了后面,因此我们可以将两个数中最后的元素拿出来依次比较,然后将较大的放在nums1中的最后面,依次类推,放到最后自然而然的可以将所有数进行排序,那怎么去实现它呢?看图说话,如下图,我们记录一个坐标i1和i2分别记录下两个数组中的最后一个元素,然后依次进行比较,如果i1中的数较大就放在下标j的位置,反之则把i2放进去,且如果nums1中走完了,nums2还没有,就只需要将nums2中的数据单独拿出来,放再j所在的位置即可。

代码实现如下:

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
    int i1 = m - 1;//记录第一个数组的尾坐标
    int i2 = n - 1;//第二个数组尾坐标
    int j = m + n - 1;//记录总元素的坐标
    while(i1 >= 0 &&  i2 >= 0)
    {
        if(nums1[i1] > nums2[i2])
        {
            nums1[j--] = nums1[i1--]; 
        }
        else
        {
            nums1[j--] = nums2[i2--];
        }
    }
    while(i2 >= 0)
    {
        nums1[j--] = nums2[i2--];
    }
}

运行结果:


结语:今天的内容就到这里吧,谢谢各位的观看,如果有讲的不好的地方也请各位多多指出,作者每一条评论都会读的,谢谢各位。


相关文章
|
28天前
|
存储 算法 程序员
【数据结构】C语言实现顺序表万字详解(附完整运行代码)
【数据结构】C语言实现顺序表万字详解(附完整运行代码)
39 0
|
30天前
|
存储 编译器
数据结构:顺序表详解
数据结构:顺序表详解
37 0
|
存储 索引
数据结构--动态顺序表
数据结构--动态顺序表
|
1月前
|
存储 消息中间件 算法
数据结构从入门到精通——顺序表
顺序表是一种常见的线性数据结构,它使用一段连续的存储单元依次存储数据元素。这种数据结构的特点是逻辑上相邻的元素在物理存储位置上也相邻,因此可以快速地访问表中的任意元素。 顺序表的实现通常依赖于数组,数组是一种静态的数据结构,一旦创建,其大小就是固定的。这意味着在顺序表中插入或删除元素可能会导致空间的浪费或不足。例如,如果在一个已经满了的顺序表中插入一个新元素,就需要重新分配更大的数组空间,并将原有元素复制到新数组中,这是一个相对耗时的操作。
54 0
|
2天前
|
存储 算法 C语言
C语言进阶:顺序表(数据结构基础) (以通讯录项目为代码练习)
C语言进阶:顺序表(数据结构基础) (以通讯录项目为代码练习)
|
11天前
|
Python
python学习-函数模块,数据结构,字符串和列表(下)
python学习-函数模块,数据结构,字符串和列表
55 0
|
1月前
|
存储 编译器
数据结构之顺序表的实现(详解!附完整代码)
数据结构之顺序表的实现(详解!附完整代码)
37 1
|
1月前
|
存储
【数据结构——顺序表的实现】
【数据结构——顺序表的实现】
|
1月前
|
存储
【数据结构】顺序表
【数据结构】顺序表
|
1月前
|
存储
数据结构之顺序表及其实现!
数据结构之顺序表及其实现!