• 顺序表与链表(上)


    目录

    线性表

    顺序表

    链表

    顺序表与链表的区别

    顺序表具体情况讲解


    线性表

    这里要讲的顺序表和链表都属于线性表。

    线性表它的逻辑结构是呈线性的,也就是单线串联下去的简单结构。

     我们所说的顺序表、链表、栈、队列、字符串都是线性表,在逻辑上是呈线性串成的。

    表,除了逻辑结构,还有物理结构。逻辑结构就是设计时候想象的情况,比如顺序表一开始想象的时候是像数组一样按顺序往后排的,链表是一个个串起来的样子.......

     我们说是不是线性表也是看它的逻辑结构。

    物理结构是实际使用的时候,呈现出的连接方式,可能逻辑结构是线性的,但物理结构就不是了。

    顺序表

    顺序表的逻辑结构和物理结构是一致的。

    顺序表实质上是数组,但数组是静态的,而顺序表可以是静态的也可以是动态的,它是动态增长的,并且要求里面存放的数据是从左往右连续的。

    比如说:创建出一个数组,可以在数组内任何位置存放数据,但是顺序表只能按顺序存入数据。

     为什么需要顺序表呢?数组创建只能一次创建固定的空间,比如arr[10],如果需要存100个数据,但是初始化的时候不知道不要多大空间,创建了1000就浪费了,创建了50就不够,这是数组的缺陷,所以需要顺序表。

    顺序表也是有缺陷的:1、如果顺序表初始化空间不够后续增加数据需要增容,但这种动态增容是有性能消耗的(如果后面没有足够的空间,开辟新空间拷贝数据,释放原空间是有消耗的)   。

        2、顺序表也是存在空间浪费的,动态扩容一次是扩容原空间两倍的空间的,比如原来是10个字节大小的空间,现在存放的数据满了,要存入再放入1个字节大小的数据,这时就扩容到20字节(2倍),这也满了,再扩容到40字节.......每次都2倍,有可能会造成一定的浪费,但这其实浪费不多,所以一般考虑较少。

    3、如果需要在头部插入数据的话,需要挪动数据,将所有数据往后挪动1位,这样做是有代价的,空间复杂度变成了O(N)。

    因此,有人发明了新的结构:链表

    链表

    链表一般是作用在结构体里面的,它是将一块一块的数据像链条一样串起来。

    比如要存一个数据5,它由两部分构成:一个是结构体数据5,后面是一个指针,指向下个节点。

     这种结构有他的优势:1、不需要动态扩容,需要插入数据直接创建,指针指向一个节点就连起来了          2、插入头指针不用挪动数据,用头指针指向第一个节点就行了。

     但链表也有缺陷,在这里先不说了。

    链表的物理结构与逻辑结构不同,不是呈线性的,因为它可以通过指针任意指向节点。

            

    顺序表与链表的区别

    上面在介绍的过程中其实就讲了区别了 。

    1、顺序表有动态增容,链表没有,所以顺序表经常会有些许空间浪费,链表因为是指针指向节点所以不存在。

    2、顺序表头部插入数据需要挪动整个数据,链表不需要,只需要用头指针指向第一个节点就行了。

    顺序表具体情况讲解

    为了方便起见,创立工程后,用3个文件来实现顺序表的不同功能。

    头文件里放结构体定义和函数头,SeqList.c实现顺序表不同功能,test.c用于测试。

    顺序表一般都是动态的,静态的用处不大,简单说一下。

    1. //静态顺序表
    2. #define N 100
    3. struct SeqList
    4. {
    5. int a[N];
    6. int size;
    7. };

     主要还是来看动态顺序表。

    1. struct SeqList
    2. {
    3. int *a; //指针指向顺序表的头
    4. int size; //size是有效数据的个数
    5. int capacity; //capacity是数据容量
    6. };

     
    *a指针指向顺序表的头, size是有效数据的个数,capacity是数据容量

     在这里数据我都定义成了int,但要是后面需要修改类型很麻烦,所以在前面我先重定义一下,方便后面修改:

    typedef int SeqDateType;

     数据结构在项目中一般是用来存储管理数据的,对内存中管理数据的结构增删查改的接口。

    这里面的接口一般有这么4个:

    1. typedef int SeqDateType;
    2. typedef struct SeqList
    3. {
    4. SeqDateType *a; //指针指向顺序表的头
    5. int size; //size是有效数据的个数
    6. int capacity; //capacity是数据容量
    7. }SEQ;
    8. void SeqListInit(SEQ seq);
    9. void SeqListDestory(SEQ seq);
    10. void SeqListPushBack(SEQ seq, SeqDateType x);
    11. void SeqListPushFront(SEQ seq, SeqDateType x);
    12. void SeqListPopBack(SEQ seq);
    13. void SeqListPopFront(SEQ seq);

    为了方便,我将结构体类型重定义为SEQ,下面4个接口分别是头插(PushFront)尾插(PushBack),头删(PopFront)尾删(PopBack)。

    数据结构里面还有2个常用的接口:初始化(Init)和销毁(Destory),因为顺序表是动态内存开辟的空间,所以用完了需要销毁,否则会内存泄漏。

    初始化(调用函数完成,不要在其他地方操作):

    注意这里要传结构体变量的地址,将seq改成*pq, 将变量的地址传给Init 。

    1. //SeqList.h
    2. void SeqListInit(SEQ *pq);
    3. void SeqListDestory(SEQ* pq);
    4. void SeqListPushBack(SEQ* pq, SeqDateType x);
    5. void SeqListPushFront(SEQ* pq, SeqDateType x);
    6. void SeqListPopBack(SEQ* pq);
    7. void SeqListPopFront(SEQ* pq);
    8. //test.c
    9. void SeqListTest1()
    10. {
    11. struct SeqList s;
    12. SeqListInit(&s);
    13. SeqListDestory(&s);
    14. }
    15. int main()
    16. {
    17. SeqListTest1();
    18. return 0;
    19. }
    20. //SeqList.c
    21. void SeqListInit(SEQ* pq)
    22. {
    23. assert(pq);
    24. pq->a = NULL;
    25. pq->size = pq->capacity = 0;
    26. }

    注意这里用了一个断言函数 assert,防止pq为空指针。

    初始化后进行增删查改等操作,完了要销毁,将指针再次置空,空间置0就行了。

    1. void SeqListDestory(SEQ *pq)
    2. {
    3. assert(pq);
    4. free(pq->a);
    5. pq->a = NULL;
    6. pq->size = pq->capacity = 0;
    7. }

    接着讲一下头尾增加数据的接口如何设计。

    尾插:

    在尾部插数据只需要把数据放在下标为size的位置(下标为size实际就是第size+1个数),然后size++就行了。要注意插入的时候size=capacity的情况,需要扩容。

    代码如下:

    1. //SeqList.h
    2. void SeqListPushBack(SEQ* pq, SeqDateType x);
    3. void SeqListPrint(SEQ* pq);
    4. //SeqList.c
    5. void SeqListPushBack(SEQ *pq, SeqDateType x)
    6. {
    7. assert(pq);
    8. if (pq->size == pq->capacity)
    9. {
    10. //扩容
    11. int newcapacity = pq->capacity * 2 == 0 ? 4: pq->capacity * 2;
    12. SeqDateType* newA = realloc(pq->a, sizeof(SeqDateType) * newcapacity);
    13. if (newA == NULL)
    14. {
    15. printf("realloc fail\n");
    16. exit(-1);
    17. }
    18. pq->a = newA;
    19. pq->capacity = newcapacity;
    20. }
    21. pq->a[pq->size] = x;
    22. pq->size++;
    23. }
    24. void SeqListPrint(SEQ*pq)
    25. {
    26. assert(pq);
    27. for (int i = 0; i < pq->size; ++i)
    28. {
    29. printf("%d ", pq->a[i]);
    30. }
    31. }
    32. //test.c
    33. void SeqListTest1()
    34. {
    35. struct SeqList s;
    36. SeqListInit(&s);
    37. SeqListPushBack(&s, 1);
    38. SeqListPushBack(&s, 2);
    39. SeqListPushBack(&s, 3);
    40. SeqListPushBack(&s, 4);
    41. SeqListPushBack(&s, 5);
    42. SeqListPrint(&s);
    43. SeqListDestory(&s);
    44. }
    45. int main()
    46. {
    47. SeqListTest1();
    48. return 0;
    49. }

    头插:

    头插和尾插一样,都要先考虑扩容的问题,因此我们可以把扩容单独做成一个接口分离出来。

    头插上面说了需要把所有数据都往后挪动,具体实现就是从后往前将前面一位的值赋给后面一位。

    用end指向末尾,每挪动一次,end--往前走一位,直到end<0。

    代码如下:

    1. //test.c
    2. //尾插1,2,3,4,5 头插 0 ,0 ,0, 0
    3. void SeqListTest1()
    4. {
    5. struct SeqList s;
    6. SeqListInit(&s);
    7. SeqListPushBack(&s, 1);
    8. SeqListPushBack(&s, 2);
    9. SeqListPushBack(&s, 3);
    10. SeqListPushBack(&s, 4);
    11. SeqListPushBack(&s, 5);
    12. SeqListPushFront(&s, 0);
    13. SeqListPushFront(&s, 0);
    14. SeqListPushFront(&s, 0);
    15. SeqListPushFront(&s, 0);
    16. SeqListPrint(&s);
    17. SeqListDestory(&s);
    18. }
    19. int main()
    20. {
    21. SeqListTest1();
    22. return 0;
    23. }
    24. //SeqList.c
    25. void SeqCheckCapacity(SEQ* pq)
    26. {
    27. if (pq->size == pq->capacity)
    28. {
    29. //扩容
    30. int newcapacity = pq->capacity * 2 == 0 ? 4 : pq->capacity * 2;
    31. SeqDateType* newA = realloc(pq->a, sizeof(SeqDateType) * newcapacity);
    32. if (newA == NULL)
    33. {
    34. printf("realloc fail\n");
    35. exit(-1);
    36. }
    37. pq->a = newA;
    38. pq->capacity = newcapacity;
    39. }
    40. }
    41. void SeqListPushFront(SEQ *pq, SeqDateType x)
    42. {
    43. assert(pq);
    44. SeqCheckCapacity(pq);
    45. int end = pq->size - 1;
    46. while (end >= 0)
    47. {
    48. pq->a[end+1] = pq->a[end];
    49. end--;
    50. }
    51. pq->a[0] = x;
    52. pq->size++;
    53. }

    增加数据会了,删除数据就很简单了。

    尾删就直接将有效数据减1就行了。

    代码如下:

    1. void SeqListPopBack(SEQ *pq)
    2. {
    3. assert(pq);
    4. assert(pq->size > 0);
    5. --pq->size;
    6. }

    头删的话,也是要挪动数据,和上面相反,往前移把第一位挪出去就行了。

    代码如下:

    1. void SeqListPopFront(SEQ *pq)
    2. {
    3. assert(pq);
    4. assert(pq->size > 0);
    5. int begin = pq->a[0];
    6. while (begin<pq->size)
    7. {
    8. pq->a[begin] = pq->a[begin+1];
    9. begin++;
    10. }
    11. pq->size--;
    12. }

    顺序表相对来说比较简单,后面的结构会越来越复杂,大家加油把!!!

  • 相关阅读:
    【预测】小米汽车电子电气架构的猜想
    【C++】日期类的实现
    OpenFeign使用示例
    CentOS8安装MySQL
    【无标题】
    Java代码审计之不安全的Java代码
    信息学奥赛研究1:竞赛时间表、学习规划
    【剑指offr--C/C++】JZ55 二叉树的深度
    Vue+Element之SpingBoot学生管理系统
    控制器宕机之SBC相关
  • 原文地址:https://blog.csdn.net/SAKURAjinx/article/details/125516021