C语言实现线性表数据结构(顺序表、链表、栈、队列)内含大量动画

C语言实现线性表数据结构

前言

线性表,顾名思义,相同类型的元素像线一样组合在一起。逻辑上,线性表是连续的,在内存上开辟一块连续的空间;然而物理结构上,线性表并不一定是连续的,数组是一块连续的空间,链式结构是一块一块的独立空间,通过指针连接,物理结构上并不一定是连续的。

线性表(List):零个多个数据元素的有限序列

顺序表图示.png

链表图示.png

顺序表

线性表的顺序储存结构指用一段地址连续的储存单元依次储存线性表的数据元素。

顺序表图示.png

由于线性表每一个数据元素的类型都相同,因此可以采用一维数组来实现顺序存储结构。即第一个元素存放在数组下标为0的位置上,其余数据依次存放在数组中相邻的位置。

静态顺序表

指定数组长度,不可扩容。

  • 静态顺序结构代码

    • #define DATA_MAX 10 // 数组长度
      typedef int SLDataType; // 重命名int
      
      typedef struct SeqList // 顺序结构结构体
      {
          SLDataType a[DATA_MAX]; // 存放元素的数组
          int size; // 数组个数
      }SeqList;
      

动态顺序表

指使用动态开辟数组存储数据元素,可扩容

  • 动态顺序结构代码

    • typedef int SLDataType;
      
      typedef struct SeqList
      {
          SLDataType* a; // 指针,指向后续动态开辟空间
          int size; // 数组个数
        	int capacity; // 动态开辟空间容量
      }SeqList;
      
  • 由于静态顺序表不具有参考性,因此下面对顺序表的操作均已动态顺序表结构进行。

顺序表初始化

使用结构体模拟顺序结构后,由于存储数据元素的空间需要动态开辟,因此需要进行顺序表中的数据进行初始化,确保size = capacity = 0SLDataType* a = NULL

  • 代码实现:

    • void InitList(SeqList* sl)
      {
          sl->a = NULL;
          sl->size = 0;
          sl->capacity = 0;
      }
      

顺序表的插入与删除

插入数据元素

无论插入数据的方式是头插还是尾插,都需要先判断存放数据元素的空间是否满了,如果满了就需要扩容。由于我们使用顺序表结构体中的size来记录元素个数,因此只要size = capacity,就代表空间已满需要扩容。

检查空间是否需要扩容
  • 代码实现

    • void CheckCapacity(SeqList* sl)
      {
          if(sl->size == sl->capacity)
          {
              // 已满,需要扩容
              int newCapacity = sl->capacity == 0 ? 10 : 2 * sl->capacity;
              SLDataType* tmp = (SLDataType*)realloc(sl->a, newCapacity * sizeof(SLDataType));
      
              if(tmp == NULL)
              {
                  perror("realloc");
                  return;
              }
              sl->a = tmp;
            	sl->capacity = newCapacity;
          }
      }
      
  • 如果sl->a == NULL,此时realloc的效果和malloc一致。

顺序表尾插数据元素

顺序表尾插动画演示.gif

  • 如图所示,size作为顺序表中元素的个数也可充当要插入元素位置的下标,即将要添加的数据元素放置到sl->a[sl->size]位置即可,在进行sl->size++改变元素个数。

  • 代码实现:

    • void SeqListPushBack(SeqList* sl, SLDataType x)
      {
          assert(sl);
      
          CheckCapacity(sl);
      
          sl->a[sl->size] = x;
          sl->size++;
      }
      
顺序表头插数据元素

顺序表头插动画演示.gif

  • 如图所示,要进行顺序表头插,需将现有的数据元素依次后移,在讲要插入的数据元素放置在sl->a[0]的位置上,sl->size++即可完成顺序表头插。

  • 代码实现:

    • void SeqListPushFront(SeqList* sl, SLDataType x)
      {
          assert(sl);
      
          CheckCapacity(sl);
      
          int end = sl->size - 1;
          while(end >= 0)
          {
              sl->a[end + 1] = sl->a[end];
              end--;
          }
          sl->a[0] = x;
          sl->size++;
      }
      
顺序表在指定下标插入数据元素
  • 首先,需要判断指定的下标是否合法,指定下标的区间应该在:

0 ⩽ p o s ⩽ s i z e 0\leqslant pos \leqslant size 0possize

  • 代码实现:

    • bool CheckPos(SeqList* sl, int pos)
      {
          if(pos < 0 || pos > sl->size)
          {
              return false;
          }
          return true;
      }
      

顺序表指定下标插入动画演示.gif

  • 如图所示,定义一个end = sl->size-1,然后在满足条件 e n d ⩾ p o s end \geqslant pos endpos,元素依次后移,再在pos位置插入元素,sl->size++即可完成顺序表在指定下标插入数据元素。

  • 代码实现:

    • void SeqListInsert(SeqList* sl, int pos, SLDataType x)
      {
          assert(sl);
      
        	CheckCapacity(sl);
          if(CheckPos(sl, pos))
          {
              int end = sl->size - 1;
              while(end >= pos)
              {
                  sl->a[end + 1] = sl->a[end];
                  end--;
              }
              sl->a[pos] = x;
              sl->size++;
              return;
          }
          printf("下标不合法\n");
          return;
      }
      
  • 根据在指定位置插入数据元素,可以更改顺序表头插和尾插,头插即在下标为0的位置上插入数据元素,尾插即在下标为sl->size上插入数据元素。

  • 更改代码如下:

    • // 头插
      void SeqListPushFront(SeqList* sl, SLDataType x)
      {
          SeqListInsert(sl, 0, x);
      }
      
      // 尾插
      void SeqListPushBack(SeqList* sl, SLDataType x)
      {
          SeqListInsert(sl, sl->size, x);
      }
      

删除数据元素

在删除数据前需要先确定顺序表中是否有数据,若顺序表已空,直接返回,不做任何操作。

  • 代码如下:

    • // 返回true表示顺序表已空,返回false表示顺序表非空。
      bool SeqListIsEmpty(SeqList* sl)
      {
          if(sl->size == 0)
          {
              return true;
          }
          return false;
      }
      
顺序表尾删数据元素
  • 顺序表尾删数据元素是非常容易的,只要在顺序表非空的前提下,sl->size--即可“删除”1末尾数据元素。

顺序表尾删动画演示.gif

  • 代码实现:

    • void SeqListPopBack(SeqList* sl)
      {
          assert(sl);
      
          if(!SeqListIsEmpty(sl))
          {
              sl->size--;
              return;
          }
          printf("顺序表已空,不能删除\n");
      }
      
顺序表头删数据元素
  • 数据表头删数据即使用后续数据元素依次向前覆盖,再进行sl->size--即可完成头删数据元素。和尾删同样,需要判断顺序表内是否有数据元素。

顺序表头删动画演示.gif

  • 代码实现:

    • void SeqListPopFront(SeqList* sl)
      {
          assert(sl);
      
          if(!SeqListIsEmpty(sl))
          {
              int end = 1;
              while(end < sl->size)
              {
                  sl->a[end - 1] = sl->a[end];
                  end++;
              }
              sl->size--;
              return;
          }
          printf("顺序表已空,不能删除\n");
      }
      
顺序表删除指定位置的数据元素
  • 如同头删数据一样,只要将后续的数据元素依次向前覆盖,再进行sl->size--即可完成删除。

顺序表指定删除动画演示.gif

  • 代码实现:

    • void SeqListErase(SeqList* sl, int pos)
      {
          assert(sl);
      
          if(!SeqListIsEmpty(sl))
          {
              int end = pos + 1;
              while(end < sl->size)
              {
                  sl->a[end - 1] = sl->a[end];
                  end++;
              }
              sl->size--;
              return;
          }
      
          printf("顺序表已空,不能删除\n");
      }
      
  • 以此,可更改顺序表头删尾删代码,头删即删除下标为0数据元素,尾删即删除下标为sl->size-1的数据元素。

  • 代码实现:

    • // 尾删
      void SeqListPopBack(SeqList* sl)
      {
          SeqListErase(sl, sl->size - 1);
      }
      
      // 头删
      void SeqListPopFront(SeqList* sl)
      {
          SeqListErase(sl, 0);
      }
      

顺序表查找数据元素

  • 顺序表查找数据元素只需要将顺序表遍历一遍,找到元素返回下标,未找到元素返回-1即可。

  • 代码实现:

    • int SeqListFind(SeqList* sl, SLDataType key)
      {
          assert(sl);
      
          int i = 0;
          for(i = 0; i < sl->size; i++)
          {
              if(sl->a[i] == key)
              {
                  return i;
              }
          }
          return -1;
      }
      

顺序表销毁

  • 由于动态顺序表是通过动态开辟内存空间的,所以需要释放空间。

  • 代码实现:

    • void SeqListDestroy(SeqList* sl)
      {
          assert(sl);
          assert(sl->a);
      
          free(sl->a);
          sl->a = NULL;
          sl->size = 0;
          sl->capacity = 0;
      }
      

链表

链表是一种物理存储结构不一定连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表图示.png

分类

  • 链表的结构非常多样,经过排列组合可有8种结构。

单项或双向

单向链表.png

双向链表.png

带头或不带头

带头链表.png

不带头链表.png

循环或非循环

循环链表.png

非循环链表.png

链表结构

  • 如上面所示,链表的结构由数据和指针组成,使用结构体实现如下:

    • typedef int LDataType;
      // 无头不循环单链表
      typedef struct LinkedList
      {
          LDataType data; // 数据
          struct LinkedList* next; // 指向下一个数据元素的指针
      }LinkedList;
      

动态申请链表结点

  • 为链表结点动态开辟一块内存空间,使用指针变量node指向该空间。赋值数据,指向下一个数据元素的指针指向NULL

  • 代码实现:

    • LinkedList* BuyNode(LDataType x)
      {
          LinkedList* node = (LinkedList*)malloc(sizeof(LinkedList));
          node->data = x;
          node->next = NULL;
      
          return node;
      }
      

链表的插入与删除

插入数据元素

  • 与顺序表需要动态扩展内存空间不同,链表每次只需要申请一个链表结点即可,不需要扩容。
链表头插数据元素
  1. 申请一个链表结点LinkedList* newNode = BuyNode(1);
  2. 让新结点的指向下一个数据元素的指针指向链表的头结点newNode->next = head;
  3. 修正头结点head = newNode;

链表头插动画演示.gif

  • 代码实现:

    • void LinkedPushFront(LinkedList* head, LDataType x)
      {
          assert(head);
      
          LinkedList* newNode = BuyNode(x);
      
          newNode->next = head;
      
          head = newNode;
      }
      
  • 但是这段代码确并没有起到应有的作用,分析原因:形参是实参的一段临时拷贝,若此处只传指针变量head,并不能改变链表结构,示意图如下。

链表二级指针分析动画演示.gif

  • 即当LinkedPushFront()函数调用结束,函数出栈,局部变量销毁,并没有改变main函数栈帧中的head,因此,此处为改变链表结构,需要传二级指针。

  • 代码实现:

    • void LinkedPushFront(LinkedList** head, LDataType x)
      {
          assert(head);
      
          LinkedList* newNode = BuyNode(x);
      
          newNode->next = *head;
      
          *head = newNode;
      }
      
链表尾插数据元素
  • 链表尾插数据,需要先找到尾部节点。由于头指针不能变动,若变动便找不到头结点,因此定义一个可移动的指针LinkedList* cur = head;。尾部节点即cur->next == NULL,此时cur指向的就是尾部结点。

链表尾插动画演示.gif

  • 代码实现:

    • void LinkedPushBack(LinkedList** head, LDataType x)
      {
          assert(head);
      
          LinkedList* newNode = BuyNode(x);
      
          if(*head == NULL)
          {
              *head = newNode;
              return;
          }
      
          LinkedList* cur = *head;
          while(cur->next != NULL)
          {
              cur = cur->next;
          }
          cur->next = newNode;
      }
      
链表在指定位置之后插入数据元素
  • 由于单向链表只能记录后一个数据元素的地址,所以在指定位置前插入数据元素需要定义两个移动指针,较麻烦,所以此处选择在指定位置后插入数据元素。

链表在指定位置后插入动画演示.gif

  • 代码实现:

    • void LinkedInsertAfter(LinkedList** head, LinkedList* pos, LDataType x)
      {
          assert(head);
          assert(pos);
      
          LinkedList* newNode = BuyNode(x);
      
          newNode->next = pos->next;
          pos->next = newNode;
      }
      

删除数据元素

链表头删数据元素
  • 链表删除数据元素需要先判断链表是否有元素,若无元素则不需要任何操作。

  • 代码实现:

    • bool LinkedIsEmpty(LinkedList** head)
      {
          assert(head);
      
          if(*head == NULL)
          {
              return true;
          }
          return false;
      }
      
  • 删除头部元素只需要定义一个指针指向头结点的下一个位置,释放掉头结点内存空间,在让头结点指针指向定义指针的位置即可完成头删。

链表头删动画演示.gif

  • 代码实现:

    • void LinkedPopFront(LinkedList** head)
      {
          assert(head);
      
          if(!LinkedIsEmpty(head))
          {
              LinkedList* cur = (*head)->next;
              free(*head);
              *head = cur;
              return;
          }
          printf("链表已空,不能删除\n");
      }
      
链表尾删数据元素
  • 定义一个指针,遍历至尾结点前一个结点。释放尾结点,对新的尾结点指针位置改成NULL

链表尾删动画演示.gif

  • 代码实现:

    • void LinkedPopBack(LinkedList** head)
      {
          assert(head);
      
          if(!LinkedIsEmpty(head))
          {
              LinkedList* cur = *head;
              if(cur->next == NULL)
              {
                  free(*head);
                  *head = NULL;
                  return;
              }
              while(cur->next->next != NULL)
              {
                  cur = cur->next;
              }
              free(cur->next);
              cur->next = NULL;
              return;
          }
          printf("链表已空,不能删除\n");
      }
      
链表在指定位置之后删除数据元素

链表在指定位置删除动画演示.gif

  • 代码实现:

    • void LinkedEraseAfter(LinkedList** head, LinkedList* pos)
      {
          assert(head);
          assert(pos);
      
          if(pos->next == NULL)
          {
              return;
          }
      
          LinkedList* del = pos->next;
          pos->next = del->next;
      
          free(del);
          del = NULL;
      }
      

链表查找数据元素

  • 查找数据元素只需要定义一个指针,遍历链表,找到数据返回该位置地址,没有找到返回NULL

  • 代码实现:

    • LinkedList* LinkedListFind(LinkedList** head, LDataType x)
      {
          assert(head);
      
          LinkedList* cur = *head;
      
          while(cur)
          {
              if(cur->data == x)
              {
                  return cur;
              }
              cur = cur->next;
          }
          return NULL;
      }
      

带头双向循环链表

结构

带头双向循环链表.png

  • 带头链表即存在一个头结点,不存放有效数据。

  • 双向链表即链表结构中有两个指针,分别指向前一个结点地址和后一个结点地址。

  • 循环链表即尾结点的下一个结点指向头结点,头结点的前一个结点指向尾结点。

  • 代码实现:

    • typedef int LDataType;
      
      typedef struct DLinkedList
      {
          LDataType data;
          struct DLinkedList* prev;
          struct DLinkedList* next;
      }DLinkedList;
      
  • 创建头结点代码实现:

    • DLinkedList* ListCreate()
      {
          DLinkedList* head = (DLinkedList*)malloc(sizeof(DLinkedList));
          head->data = -1;
          head->prev = head;
          head->next = head;
      
          return head;
      }
      

带头双向循环链表的插入与删除

插入数据元素

链表头插数据元素

带头双向循环链表头插动画演示.gif

  • 代码实现:

    • void ListPushFront(DLinkedList* head, LDataType x)
      {
          assert(head);
      
          DLinkedList* node = BuyNode(x);
      
          DLinkedList* next = head->next;
      
          node->next = next;
          next->prev = node;
          node->prev = head;
          head->next = node;
      }
      
链表尾插数据元素

带头双向循环链表尾插动画演示.gif

  • 代码实现:

    • void ListPushBack(DLinkedList* head, LDataType x)
      {
          assert(head);
      
          DLinkedList* node = BuyNode(x);
      
          node->prev = head->prev;
          head->prev->next = node;
          head->prev = node;
          node->next = head;
      }
      
链表在指定位置前插入数据元素

带头双向循环链表在任意位置插入动画演示.gif

  • 代码实现:

    • void ListInsert(DLinkedList* pos, LDataType x)
      {
          assert(pos);
      
          DLinkedList* node = BuyNode(x);
      
          DLinkedList* prev = pos->prev;
          prev->next = node;
          node->prev = prev;
          node->next = pos;
          pos->prev = node;
      }
      

删除数据元素

链表头删数据元素

带头双向循环链表头删动画演示.gif

  • 代码实现:

    • void ListPopFront(DLinkedList* head)
      {
          assert(head);
          assert(head->next != head);
      
          DLinkedList* next = head->next;
      
          head->next = next->next;
          next->next->prev = head;
      
          free(next);
      
      }
      
链表尾删数据元素

带头双向循环链表尾删动画演示.gif

  • 代码实现:

    • void ListPopBack(DLinkedList* head)
      {
          assert(head);
      
          assert(head->next != head);
      
          DLinkedList* tail = head->prev;
      
          DLinkedList* tailPrev = tail->prev;
      
          free(tail);
      
          head->prev = tailPrev;
          tailPrev->next = head;
      }
      
链表在指定位置前删除数据元素

带头双向循环链表任意位置删除动画演示.gif

  • 代码实现:

    • void ListErase(DLinkedList* pos)
      {
          assert(pos);
      
          DLinkedList* prev = pos->prev;
          DLinkedList* prePrev = prev->prev;
      
          prePrev->next = pos;
          pos->prev = prePrev;
      
          free(prev);
      }
      

链表查找数据元素

  • 由于是循环链表,所以在遍历链表的过程中找到结束位置,也就是循环至尾结点(头结点前一个结点),即cur->next == head,此处cur结点就是尾结点。

  • 代码实现:

    • DLinkedList* ListFind(DLinkedList* head, LDataType x)
      {
          assert(head);
          assert(head->next != head);
      
          DLinkedList* cur = head->next;
      
          while(cur->next != head)
          {
              if(cur->data == x)
              {
                  return cur;
              }
              cur = cur->next;
          }
          return NULL;
      }
      

顺序表和链表的区别

不同点顺序表链表
存储空间上物理上一定连续物理上不一定连续
随机访问时间复杂度O(1)时间复杂度O(N)
任意位置插入或删除元素由于需要移动元素,效率低,时间复杂度O(N)只需要改变指针
插入需要考虑扩容不需要考虑扩容
应用场景频繁访问、元素高效存储任意位置插入和删除频繁
缓存利用率

栈(Stack)是限定仅在表尾进行插入和删除操作的线性表。

  • 允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。
  • 栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。

栈动画演示.gif

模拟栈结构

数组栈结构

数组栈结构.png

链表栈结构

链表栈结构.png

对比数组栈结构和链表栈结构,进栈和出栈的时间复杂度都是O(1)。空间上,数组栈结构可能存在内存空间上的浪费,但存取时定位很方便。

链表栈结构要求每一个元素都有指针域,增加了内存的开销,但对于栈的长度无限制。

因此,如果栈的使用过程中元素变化不可预料,有时很小,有时很大,那么最好用链表栈结构,反之,如果它的变化在可控范围内,建议使用数组栈结构。

  • 代码实现:

    • // 静态栈(存储数量固定)
      #define N 10
      typedef int STDataType;
      
      typedef struct Stack
      {
        	STDataType a[N];
        	int top;
      }Stack;
      
    • 静态栈无法扩容,只能存储固定数量的数据。

    • // 动态栈
      typedef int STDataType;
      
      typedef struct Stack
      {
          STDataType* a; // 指针指向存储数据的数组
          int top; // 栈顶
          int capacity; // 数组容量
      }Stack;
      

栈的初始化

  • 创建栈结构体Stack stack,初始化stack->a = NULLstack->top = 0stack->capacity = 0

  • 代码实现:

    • void StackInit(Stack* stack)
      {
          assert(stack);
      
          stack->a = NULL;
          stack->top = 0;
          stack->capacity = 0;
      }
      

压栈/入栈

  • 由于存储数据的是动态开辟数组,所以在压栈之前,都需要先判定栈空间是否已满。即stack->top == stack->capacity代表空间已满,需要扩容。

  • 代码实现:

    • bool CheckCapacity(Stack* stack)
      {
          if(stack->top == stack->capacity)
          {
              return true;
          }
          return false;
      }
      

栈压栈动画演示.gif

  • 如图所示,只需要将压栈数据元素放在数组stack->atop下标下即可完成压栈。

  • 代码实现:

    • void StackPush(Stack* stack, STDataType x)
      {
          assert(stack);
      
          if(CheckCapacity(stack))
          {
              int newCapacity = stack->capacity == 0 ? 4 : 2 * stack->capacity;
      
              STDataType* tmp = (STDataType*)realloc(stack->a, newCapacity * sizeof(STDataType));
              if(tmp == NULL)
              {
                  perror("realloc");
                  return;
              }
      
              stack->a = tmp;
              stack->capacity = newCapacity;
          }
      
          stack->a[stack->top] = x;
          stack->top++;
      }
      

弹栈/出栈

  • 在弹栈之前,需要先判断栈是否为空,若栈为空,提示栈已空。栈不为空,弹出栈顶元素2

  • 判断栈是否已空代码实现:

    • bool StackIsEmpty(Stack* stack)
      {
          if(stack->top == 0)
          {
              return true;
          }
          return false;
      }
      

栈弹栈动画演示.gif

  • 代码实现:

    • void StackPop(Stack* stack)
      {
          // 弹出栈顶元素
          assert(stack);
      
          if(!StackIsEmpty(stack))
          {
              stack->top--;
              return;
          }
          printf("栈已空,不能弹栈"); 
      }
      

栈顶元素

  • 访问栈顶元素,首先还是判断栈是否为空,若为空直接报错。不为空只要访问stack->a[stack->top - 1]元素即可。

  • 代码实现:

    • STDataType StackTop(Stack* stack)
      {
          assert(stack);
      
          assert(!StackIsEmpty(stack));
      
          return stack->a[stack->top - 1];
      }
      

栈内元素个数

  • stack->top插入元素位置下标,也可作为栈内元素个数。

  • 代码实现:

    • int StackSize(Stack* stack)
      {
          assert(stack);
      
          return stack->top;
      }
      

栈的销毁

  • 由于存储数据元素的数组是动态开辟的内存空间,因此需要释放内存空间。

  • 代码实现:

    • void StackDestroy(Stack* stack)
      {
          assert(stack);
      
          free(stack->a);
          stack->top = 0;
          stack->capacity = 0;
      }
      

队列

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

  • 队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

队列动画演示.gif

模拟队列结构

由于队列的特性是先进先出,使用数组模拟队列结构在删除数据的时候需要移动数组中元素,时间复杂度O(N)。而使用链表在删除数据时只要采用头删法,时间复杂度O(1)。因此,我们采用链表来模拟队列结构。

队列链表模拟结构.png

  • 代码实现:

    • // 队列节点
      typedef int QDataType;
      
      typedef struct QNode
      {
          QDataType data;
          struct QNode* next;
      }QNode;
      
    • 队列结点.png

    • // 队列结构
      typedef struct Queue
      {
          QNode* head; // 指向队头
          QNode* tail; // 指向队尾
      }Queue;
      
    • 队列结构.png

初始化队列

  • 初始化队列,使队头和队尾指针均指向NULL

  • 代码实现:

    • void QueueInit(Queue* pq)
      {
          assert(pq);
      
          pq->head = pq->tail = NULL;
      }
      

数据元素入队列

  1. 创建一个队列结点指针QNode* newNode,指向要入队列的数据元素。
  2. 若队列中没有元素,即pq->tail == NULL,要入队列的数据元素就作为首元素,队头指针和队尾指针均指向该元素。
  3. 若队列中已有元素,即pq->tail != NULL,只需要修改队尾指针的next指向新元素,再修改队尾指针指向即可。

队列入队列动画演示.gif

  • 代码实现:

    • void QueuePush(Queue* pq, QDataType x)
      {
          assert(pq);
      
          QNode* newNode = (QNode*)malloc(sizeof(QNode));
      
          if(newNode == NULL)
          {
              perror("malloc");
              return;
          }
          newNode->data = x;
          newNode->next = NULL;
      
          if(pq->tail == NULL)
          {
              pq->head = pq->tail = newNode;
          }
          else
          {
              pq->tail->next = newNode;
              pq->tail = newNode;
          }
      }
      

数据元素出队列

  1. 判断队列是否为空,若为空,直接报错。

    • 判断队列是否为空代码实现:

      • bool QueueIsEmpty(Queue* pq)
        {
            return pq->head == NULL;
        }
        
  2. pq->head->next == NULL,说明队列中只有一个元素,只要释放掉队头元素,再把pq->head = pq->tail = NULL即可。

  3. 若队列中不止一个元素,只需要释放掉队头元素,再让队头指针指向下一个数据元素即可完成出队列操作。

队列出队列动画演示.gif

  • 代码实现

    • void QueuePop(Queue* pq)
      {
          assert(pq);
          assert(!QueueIsEmpty(pq));
      
          if(pq->head->next == NULL)
          {
              free(pq->head);
              pq->head = pq->tail = NULL;
          }
          else
          {
              QNode* next = pq->head->next;
              free(pq->head);
              pq->head = next;
          }   
      }
      

获取队头元素和队尾元素

  • 只需要判断队列是否为空,然后返回pq->head->datapq->tail->data即可。

  • 代码实现:

    • QDataType QueueTop(Queue* pq)
      {
          assert(pq);
          assert(!QueueIsEmpty(pq));
      
          return pq->head->data;
      }
      
      QDataType QueueTail(Queue* pq)
      {
          assert(pq);
          assert(!QueueIsEmpty(pq));
          
          return pq->tail->data;
      }
      

队列中元素个数

  • 采用计数法,来计算队列中元素个数。

  • 代码如下:

    • int QueueSize(Queue* pq)
      {
          assert(pq);
          int count = 0;
          QNode* cur = pq->head;
      
          while(cur)
          {
              count++;
              cur = cur->next;
          }
          return count;
      }
      

队列的销毁

  • 代码实现:

    • void QueueDestroy(Queue* pq)
      {
          assert(pq);
      
          QNode* cur = pq->head;
          while(cur)
          {
              QNode* curNext = cur->next;
              free(cur);
              cur = curNext;
          }
          pq->head = pq->tail = NULL;
          
      }
      

循环队列

循环队列.png

逻辑上,循环队列如上图所示,首尾相连。物理结构上,可以使用数组实现,也可以使用循环链表实现。

  • 此处我们采用数组实现循环队列,并设置一个空位点,作为队列已满的标志,即tail + 1 == head

循环队列结构

循环队列动画演示.gif

  • 结构代码实现:

    • typedef struct {
          int* a;
          int head; // 队头下标
          int tail; // 队尾下标
          int k; // 队列长度
      } MyCircularQueue;
      

创建循环队列

  • 代码实现:

    • MyCircularQueue* myCircularQueueCreate(int k) {
          MyCircularQueue* cq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
          cq->a = (int*)malloc(sizeof(int) * (k + 1));
          cq->head = 0;
          cq->tail = 0;
          cq->k = k;
      
          return cq;
      }
      

插入数据元素

  • 循环队列插入数据元素前,需要先判断队列是否满了,若队列满了,插入失败,返回false,插入成功,返回true

  • 判断队列是否满了代码实现:

    • bool myCircularQueueIsFull(MyCircularQueue* obj) {
          int cur = obj->tail + 1;
          cur %= (obj->k + 1); // 由于是循环队列,队尾下标+1可能超出队列范围,所以需要修正下标。
          return cur == obj->head;
      }
      
  • 数据元素插入在obj->a[obj->tail]位置上,插入成功后obj->tail++,但有可能队尾下标超出界限,需要修正下标。

  • 代码实现:

    • bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
          if(myCircularQueueIsFull(obj))
          {
              return false;
          }
          obj->a[obj->tail] = value;
          obj->tail++;
          obj->tail %= (obj->k + 1);
          return true;
      }
      

删除数据元素

  • 在删除数据元素前,需要判断队列是否为空,若队列为空,删除失败,返回false,删除成功,返回true

  • 判断队列是否为空代码实现:

    • bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
          return obj->head == obj->tail;
      }
      
  • 删除数据元素,只需要控制head下标即可,最后为了不使下标超出范围,修正下标。

  • 代码实现:

    • bool myCircularQueueDeQueue(MyCircularQueue* obj) {
          if(myCircularQueueIsEmpty(obj))
          {
              return false;
          }
          obj->head++;
          obj->head %= (obj->k + 1);
          return true;
      }
      

获取队头元素

  • 只需要判断队列是否为空,再考虑返回obj->a[obj->head]

  • 代码实现:

    • int myCircularQueueFront(MyCircularQueue* obj) {
          if(myCircularQueueIsEmpty(obj))
          {
              return -1;
          }
          return obj->a[obj->head];
      }
      

获取队尾元素

  • 需要分两种情况,第一种就是obj->tail != 0,只需要返回obj->a[obj->tail]即可;第二种就是obj->tail = 0,这样队尾元素的下标就需要加上队列长度,即obj->tail - 1 + obj->k + 1

  • 代码实现:

    • int myCircularQueueRear(MyCircularQueue* obj) {
          if(myCircularQueueIsEmpty(obj))
          {
              return -1;
          }
          if(obj->tail - 1 < 0)
          {
              return obj->a[obj->tail - 1 + obj->k + 1];
          }
          return obj->a[obj->tail - 1];
      }
      

队列的销毁

  • 循环队列分别开辟了存放数据的数组的内存空间和队列结构的内存空间,因此需要分别释放。

  • 代码实现:

    • void myCircularQueueFree(MyCircularQueue* obj) {
          free(obj->a);
          obj->a = NULL;
          free(obj);
          obj = NULL;
      }
      

  1. 此处删除并不是真正把数据元素从顺序表中移除出去,而是借由sl->size访问元素时,暂时不访问要删除位置的数据元素 ↩︎

  2. 并不是真的弹出栈顶元素,只是不再访问该位置的数据元素。再次压栈时,将该位置数据元素覆盖。 ↩︎

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烛九_阴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值
>