【初阶数据结构】——带头双向循环链表(C描述)

简介: 【初阶数据结构】——带头双向循环链表(C描述)

前言

上一篇文章我们学习了单链表,同时我们提到了链表其实有很多种结构:带头或不带头,循环或不循环。

但其实,最常用的还是两种结构:

7889cf25058a4475acc130695d8158c9.png

上一篇文章我们已经学了单链表(不带头),那这篇文章,我们就来学习一下带头双向循环链表

带头双向循环链表实现

1. 结构介绍

首先,从结构上来说,带头双向循环链表是结构最复杂的:

e42bc81c814b41e9a5449a2f4897ae34.png

它带哨兵位的头结点,还是双向的,还循环。

带头双向循环链表一般用来单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。

对于带头双向循环链表来说:

首先它是带哨兵位的头结点的,也就是说,它是空表状态的时候,也是有一个头结点存在的(当然它不存储有效数据)。

对于它的每个结点来说,首先它要能存储一个数据,然后呢?它需要有两个指针,一个存它前驱的地址,另一个存它后继的地址。

typedef int DLDataType;
typedef struct DoubleListNode
{
  struct DoubleListNode* prev;
  DLDataType data;
  struct DoubleListNode* next;
}DLNode;

它的结构虽然复杂,但是呢,使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

接下来我们就来实现一下对应的接口函数。

2. 结点创建

带头双向循环链表的每个结点:一个数据域,两个指针域。

//创建结点
DLNode* CreateNewnode(DLDataType x)
{
  DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
  if (newnode == NULL)
  {
    perror("malloc fail\n");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  newnode->prev = NULL;
  return newnode;
}

相信经过单链表的学习,这个函数就不用给大家过多解释了。

3. 初始化

大家如果看了上一篇单链表的文章会发现我们在实现单链表的时候其实没有搞初始化单链表的函数。

为什么没有搞呢?

因为不需要,我们实现的是单链表,而且不带头。

对于这样一个单链表只要有一个头指针就行了,空表怎么表示,是不是头指针指向空就行了啊,创建链表的时候是不是直接在头指针后面链接结点就行了。

根本不用写初始化的函数,定义一个头指针就完事了。


那为什么我们今天实现的带头双向循环链表还要搞一个初始化的函数呢?

因为它是带头的,所以我们应该先初始化一下,先搞一个头结点出来,就算是空的状态也应该有一个头存在的,后面要插入新结点是不是应该基于头结点进行操作啊。

那初始化函数应该怎么写?

搞一个头结点就完事了吗?首先哨兵位的头结点不存储有效数据,它的数据域我们可以随便给个值。

那指针域呢?

我们新创建的结点指针域默认赋值为NULL,是指向空的。那头结点的指针域我们需要改动吗?

我们来思考一下:

头结点的两个指针域(prev,next)

4c6b3c6df3e0426180c5f22560b20f53.png

它的prev应该是指向尾结点的,那现在还没有有效结点,只有哨兵位自己,那它自己就是尾,那就让prev指向它自己。

那next呢?

如果指向空的话,是不是好像没有循环起来啊,到空就断了。而且现在只有一个哨兵位的头结点,它自己就是尾,尾的next应该指向头,而现在头也是它自己,所以我们初始化的时候让next也指向自己。

我们初始化的时候让头结点的两个指针域都指向自己,首先这样更符合循环,其次这样做在后面的操作中会带来很大的优势,我们到后面就能体会到。

所以,初始化的函数应该是这样的:

//初始化
DLNode* DLInit()
{
  DLNode* guard = CreateNewnode(-1);
  //创建哨兵位的头结点(不存储有效数据)
  guard->next = guard;
  guard->prev = guard;
  return guard;
}

另外这里再给大家提一点:


我们看到初始化函数我们搞了一个返回值,为什么这样做呢?

因为我们初始化之后,有一个哨兵位的头结点在这里,我们需要有一个头指针来指向这个头结点,以便我们来访问链表。

所以我们初始化的函数需要有一个返回值,返回哨兵位结点的地址,让我们自己的指针指向它,这样就能访问这个链表。

当然,除了返回值的方法,也可以用二级指针(传头指针的地址), 这里就不实现了。

但是传一级肯定是不行的,因为形参的改变并不会影响实参。

4. 销毁

和单链表一样,每个结点的空间是我们使用malloc动态开辟的,所以是需要我们手动去释放的。

很简单,还是对链表进行遍历,一一释放每个结点。

但是,对于带头双向循环链表,遍历结束的条件是什么呢?

这一点就和单链表不一样了,我们遍历单链表的时候,定义一个cur,走到空结束,因为单链表尾结点的指针域存的是NULL。

但是,对于循环链表来说,每个结点的指针域都没有空,那怎么判断遍历结束呢?

我们要知道循环链表是怎么循环起来的,是尾结点的next指针存了头结点的地址,头结点的prev指针存了尾结点的地址。

所以:

我们定义一个指针(cur ),从哨兵位后面的第一个有效结点开始向后走(cur =cur->next),直到cur == phead时循环结束,然后再把头结点释放一下:free(phead);,就销毁完了。

f2e17747f2b04fdeaadd782cc0cba7c1.png

正常情况下,phead不可能为空(即使是空表,phead指向头结点,也不为空),所以我们进行一个断言:

assert(phead);

//销毁
void DLDestory(DLNode* phead)
{
  assert(phead);
  DLNode* cur = phead->next;
  while (cur != phead)
  {
    DLNode* next = cur->next;
    free(cur);
    cur = next;
  }
  free(phead);
  phead = NULL;
}

对空表的时候也适用,空的时候只有哨兵位的头结点,phead指向头结点,cur = phead->next,而我们初始化的时候头结点的next就指向自己。这样cur==phead,直接就不会进while循环,只销毁一下头结点,就完事了。

如果我们初始化的时候没有让头结点的next指向自己,这样cur = phead->next之后cur 就是NULL了,就进去while循环了,反而会出现问题了。

另外:

这里我们释放完哨兵头之后虽然phead = NULL;,把头指针置空了,但其实并不会影响外面真正的头指针,还是形参与实参的关系。

所以其实函数内部这句phead = NULL;加不加都无所谓,因为函数调用结束这个指针变量也就销毁了,当然加上是一个好习惯。

但是需要我们在函数外部手动将真正的头指针置一下空,不置的话就是一个空指针了。

5. 头插

带头双向循环链表的头插也非常简单,给大家看图比较直观一下:

4381b10e5c584d45b61d249bb13f8208.png

void DLPushFront(DLNode* phead, DLDataType x)
{
  assert(phead);
  //DLNode* newnode = CreateNewnode(x);
  //如果不保存第一个结点的地址,改变指针指向需要注意顺序
  /*newnode->next = phead->next;
  phead->next->prev = newnode;
  phead->next = newnode;
  newnode->prev = phead;*/
  //顺序无关
  DLNode* first = phead->next;//保存第一个结点的地址
  //phead newnode first
  phead->next = newnode;
  newnode->prev = phead;
  newnode->next = first;
  first->prev = newnode;
}

6. 头删

d4e0b553ffbd49e9895e31532f6d62a2.png

头删的时候要注意进行一个判断:


如果链表为空了,就不能再删了,那怎么判断带头双向循环链表为空呢?

如果哨兵位的头结点的prev和next指针域都指向自己,是不是就是空表啊。

所以,我们可以加一个断言:

assert(phead->next != phead);

然后,改变对应的指针指向,释放结点就行了。

//头删
void DLPopFront(DLNode* phead)
{
  assert(phead);
  assert(phead->next != phead);
  DLNode* first = phead->next;
  DLNode* second = first->next;
  //phead first second
  phead->next = second;
  second->prev = phead;
  free(first);
  first = NULL;
}

7. 尾插

我们先来回忆一下,我们上一篇文章实现的单链表,它的尾插尾删其实是很麻烦的:


单链表尾插还要进行一个判断,因为我们实现的是没有头结点的,如果是对空表尾插,直接将要插入的结点赋值给头指针即可。

但对于不是空表的尾插,还要要先遍历找尾,然后让尾结点的指针域存新结点的地址,使其成为新的尾。


但是带头双向循环链表的尾插需要这么麻烦吗?


不需要的,带头双向循环链表的尾插尾删实现起来就爽多了。

首先它是带头的,空表头插我们也不需要像单链表那样单独处理,其次,单链表尾插我们还需要遍历找尾,但是对于循环链表来说,找尾简单吗?

是不是so easy啊。头结点的prev指向的不就是尾嘛,找尾一句代码就搞定了:

DLNode* tail = phead->prev;

那接下来插入就很简单了,还是对指针的改变:

9e98fa39675640a884574b00c10c9bcc.png

上代码:

//尾插
void DLPushBack(DLNode* phead, DLDataType x)
{
  assert(phead);
  DLNode* newnode = CreateNewnode(x);
  DLNode* tail = phead->prev;
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;
}

8. 尾删

尾删呢也很简单:

880824370c11422b94f8d8a1560c8e46.png

//尾删
void DLPopBack(DLNode* phead)
{
  assert(phead);
  assert(phead->next != phead);//如果为空,就不能再删了
  DLNode* tail = phead->prev;
  DLNode* newtail = tail->prev;
  phead->prev = newtail;
  newtail->next = phead;
  free(tail);
  tail = NULL;
}

我们单链表尾删的时候对于只剩一个结点的情况还需要单独判断,但是对于带头双向循环链表只有一个有效结点时头删也不需要单独判断,我们直接删就行了,删完就变成初始化的状态了。

eff3a6ad298e4929bdbc9f769d533240.png

9. 打印

打印呢也很好搞:


对链表进行遍历,打印每个结点中的数据就行了。

而遍历结束的条件,我们在实现销毁的时候是不是就讨论过了呀,定义一个指针(cur ),从哨兵位后面的第一个有效结点开始向后走(cur =cur->next),直到cur == phead时循环结束就行了。

63ee68974ae442759ed767f1ed177628.png

//打印
void DLPrint(DLNode* phead)
{
  assert(phead);
  DLNode* cur = phead->next;
  while (cur != phead)
  {
    printf("%d ", cur->data);
    cur = cur->next;
  }
  printf("\n");
}

10. 查找

查找是不是还是对链表进行遍历啊。

假设我们要查找的元素是X,那就遍历链表,将每个结点的值与X进行对比,相同的时候就是找到了,我们可以返回该结点的地址,如果找不到,我们就返回一个NULL。

d239417824ca40c789e6d1dacfc318ce.png

//查找
DLNode* DLFind(DLNode* phead, DLDataType x)
{
  assert(phead);
  DLNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}

11. 在pos之前插入数据

我们直接看图:

25e6ccc56caa4b87b980ce0e2b14a2ae.png

那实现起来也很简单,创建一个新结点,它的数据域赋值为我们要插入的数据,然后链接起来就行了.

那有没有什么需要注意的呢?

pos是不是得是个有效的位置啊,所以我们要加一个断言:

assert(pos);

我们这个函数是和查找函数结合使用的,find函数给我们返回一个地址,我们把它传给当前函数,在该位置前面插入.

如果pos为空,说明在链表中都找不到,那还往哪插呢?

//在pos之前插入(头插尾插可以复用)
void DLInsert(DLNode* pos, DLDataType x)
{
  assert(pos);
  DLNode* pos_prev = pos->prev;
  DLNode* newnode = CreateNewnode(x);
  //pos_prev newnode pos
  pos_prev->next = newnode;
  newnode->prev = pos_prev;
  newnode->next = pos;
  pos->prev = newnode;
}

这个函数实现好之后,我们会发现:

我们前面实现的头插尾插是不是可以复用这个函数啊,因为DLInsert函数是在pos位置之前插入,这个pos可以是链表中任意一个有效位置啊,那当然可以在头尾进行插入了.

那头插尾插就可以这样简化了.

头插:

DLInsert(phead->next, x);

尾插:

DLInsert(phead, x);

尾插为什么传的是phead呢?

phead是指向头结点的指针,而在循环链表中,头结点的前面位置不就是尾嘛.

12. 删除pos位置

直接来看图:

e3b7e44523c746b89cb1ff87a79ca240.png

这里的pos位置有没有什么限制啊?

它不能是哨兵位的头结点,既然是带头的链表,那头结点我们肯定不能删.

不过如果我们是把find的返回值传给pos , pos也不会是头结点,因为我们遍历都是从头结点后面开始的.

//删除pos位置(头插头删可以复用)
void DLErase(DLNode* pos)
{
  assert(pos);
  DLNode* pos_prev = pos->prev;
  DLNode* pos_next = pos->next;
  //pos->prev pos pos->next
  pos_prev->next = pos_next;
  pos_next->prev = pos_prev;
  free(pos);
  pos = NULL;
}

那这个函数写好,头删尾删是不是也可以复用:

头删:

DLErase(phead->next);

尾删:

DLErase(phead->prev);

所以我们以后再写的时候可以先写这两个函数,然后头部尾部的插入删除就可以直接复用了.

13. 判空

判空的话呢,也很简单:

如果头结点的prev或next指针域指向的是自己,是不是就代表此时是空的状态啊。

6ebc14cc110849b2bd7edcdc4a1f442f.png

//判空
bool DLEmpty(DLNode* phead)
{
  assert(phead);
  return phead->next == phead;
}

当然,我们这里函数的返回值用的bool类型,需要包含一个头文件:

#include <stdbool.h>

14. 计算大小

计算大小,那就遍历一下链表,计算一下元素个数就行了.

//计算大小
int DLSize(DLNode* phead)
{
  assert(phead);
  DLNode* cur = phead->next;
  int size = 0;
  while (cur != phead)
  {
    size++;
    cur = cur->next;
  }
  return size;
}

源码展示

1. DoubleList.h

(头文件的包含、结构定义和函数声明)

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int DLDataType;
typedef struct DoubleListNode
{
  struct DoubleListNode* prev;
  DLDataType data;
  struct DoubleListNode* next;
}DLNode;
//创建结点
DLNode* CreateNewnode(DLDataType x);
//初始化
DLNode* DLInit();
//销毁
void DLDestory(DLNode* phead);
//打印
void DLPrint(DLNode* phead);
//头插
void DLPushFront(DLNode* phead, DLDataType x);
//头删
void DLPopFront(DLNode* phead);
//尾插
void DLPushBack(DLNode* phead, DLDataType x);
//尾删
void DLPopBack(DLNode* phead);
//查找
DLNode* DLFind(DLNode* phead, DLDataType x);
//在pos之前插入
void DLInsert(DLNode* pos, DLDataType x);
//删除pos位置
void DLErase(DLNode* pos);
//判空
bool DLEmpty(DLNode* phead);
//计算大小
int DLSize(DLNode* phead);

2. DoubleList.c

(函数具体实现)

#define _CRT_SECURE_NO_WARNINGS
#include "DoubleList.h"
//创建结点
DLNode* CreateNewnode(DLDataType x)
{
  DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
  if (newnode == NULL)
  {
    perror("malloc fail\n");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  newnode->prev = NULL;
  return newnode;
}
//初始化
DLNode* DLInit()
{
  DLNode* guard = CreateNewnode(-1);//创建哨兵位的头结点(不存储有效数据)
  guard->next = guard;
  guard->prev = guard;
  return guard;
}
//销毁
void DLDestory(DLNode* phead)
{
  assert(phead);
  DLNode* cur = phead->next;
  while (cur != phead)
  {
    DLNode* next = cur->next;
    free(cur);
    cur = next;
  }
  free(phead);
  phead = NULL;
}
//打印
void DLPrint(DLNode* phead)
{
  assert(phead);
  DLNode* cur = phead->next;
  while (cur != phead)
  {
    printf("%d ", cur->data);
    cur = cur->next;
  }
  printf("\n");
}
//头插
void DLPushFront(DLNode* phead, DLDataType x)
{
  assert(phead);
  //DLNode* newnode = CreateNewnode(x);
  //如果不保存第一个结点的地址,改变指针指向需要注意顺序
  /*newnode->next = phead->next;
  phead->next->prev = newnode;
  phead->next = newnode;
  newnode->prev = phead;*/
  顺序无关
  //DLNode* first = phead->next;//保存第一个结点的地址
  phead newnode first
  //phead->next = newnode;
  //newnode->prev = phead;
  //newnode->next = first;
  //first->prev = newnode;
  DLInsert(phead->next, x);//复用DLInsert
}
//头删
void DLPopFront(DLNode* phead)
{
  assert(phead);
  assert(phead->next != phead);
  //DLNode* first = phead->next;
  //DLNode* second = first->next;
  phead first second
  //phead->next = second;
  //second->prev = phead;
  //free(first);
  //first = NULL;
  DLErase(phead->next);//复用DLErase
}
//尾插
void DLPushBack(DLNode* phead, DLDataType x)
{
  assert(phead);
  //DLNode* newnode = CreateNewnode(x);
  /*DLNode* tail = phead->prev;
  tail->next = newnode;
  newnode->prev = tail;
  newnode->next = phead;
  phead->prev = newnode;*/
  DLInsert(phead, x);//复用DLInsert
}
//尾删
void DLPopBack(DLNode* phead)
{
  assert(phead);
  assert(phead->next != phead);//如果为空,就不能再删了
  /*DLNode* tail = phead->prev;
  DLNode* newtail = tail->prev;
  phead->prev = newtail;
  newtail->next = phead;
  free(tail);
  tail = NULL;*/
  DLErase(phead->prev);//复用DLErase
}
//查找
DLNode* DLFind(DLNode* phead, DLDataType x)
{
  assert(phead);
  DLNode* cur = phead->next;
  while (cur != phead)
  {
    if (cur->data == x)
    {
      return cur;
    }
    cur = cur->next;
  }
  return NULL;
}
//在pos之前插入(头插尾插就可以复用了)
void DLInsert(DLNode* pos, DLDataType x)
{
  assert(pos);
  DLNode* pos_prev = pos->prev;
  DLNode* newnode = CreateNewnode(x);
  //pos_prev newnode pos
  pos_prev->next = newnode;
  newnode->prev = pos_prev;
  newnode->next = pos;
  pos->prev = newnode;
}
//删除pos位置(头插头删就可以复用了)
void DLErase(DLNode* pos)
{
  assert(pos);
  DLNode* pos_prev = pos->prev;
  DLNode* pos_next = pos->next;
  //pos->prev pos pos->next
  pos_prev->next = pos_next;
  pos_next->prev = pos_prev;
  free(pos);
  pos = NULL;
}
//判空
bool DLEmpty(DLNode* phead)
{
  assert(phead);
  return phead->next == phead;
}
//计算大小
int DLSize(DLNode* phead)
{
  assert(phead);
  DLNode* cur = phead->next;
  int size = 0;
  while (cur != phead)
  {
    size++;
    cur = cur->next;
  }
  return size;
}

3. Test.c

(对函数功能的测试)

#define _CRT_SECURE_NO_WARNINGS
#include "DoubleList.h"
#include <stdio.h>
void test1()
{
  DLNode* phead = DLInit();
  DLPushFront(phead, 1);
  DLPushFront(phead, 2);
  DLPushFront(phead, 3);
  DLPushFront(phead, 4);
  DLPushFront(phead, 5);
  DLPrint(phead);
  DLPopFront(phead);
  DLPrint(phead);
  DLPopFront(phead);
  DLPrint(phead);
  DLPopFront(phead);
  DLPrint(phead);
  DLPopFront(phead);
  DLPrint(phead);
  DLPopFront(phead);
  DLPrint(phead);
  DLDestory(phead);
  phead = NULL;
}
void test2()
{
  DLNode* phead = DLInit();
  DLPushBack(phead, 1);
  DLPushBack(phead, 2);
  DLPushBack(phead, 3);
  DLPushBack(phead, 4);
  DLPushBack(phead, 5);
  DLPrint(phead);
  DLPopBack(phead);
  DLPrint(phead);
  DLPopBack(phead);
  DLPrint(phead);
  DLPopBack(phead);
  DLPrint(phead);
  DLPopBack(phead);
  DLPrint(phead);
  DLPopBack(phead);
  DLPrint(phead);
  DLDestory(phead);
  phead = NULL;
}
void test3()
{
  DLNode* phead = DLInit();
  DLPushBack(phead, 1);
  DLPushBack(phead, 2);
  DLPushBack(phead, 3);
  DLPushBack(phead, 4);
  DLPushBack(phead, 5);
  DLPrint(phead);
  DLNode* pos = DLFind(phead, 3);
  /*if (pos)
  {
    pos->data = 10;
  }*/
  if (pos)
  {
    DLInsert(pos, 99);
  }
  DLPrint(phead);
  /*if (pos)
  {
    printf("找到了\n");
  }
  else
  {
    printf("找不到\n");
  }*/
  DLDestory(phead);
  phead = NULL;
}
void test4()
{
  DLNode* phead = DLInit();
  DLPushBack(phead, 1);
  DLPushBack(phead, 2);
  DLPushBack(phead, 3);
  DLPushBack(phead, 4);
  DLPushBack(phead, 5);
  DLPrint(phead);
  DLNode* pos = DLFind(phead, 3);
  if (pos)
  {
    DLErase(pos);
  }
  DLPrint(phead);
  DLDestory(phead);
  phead = NULL;
}
void test5()
{
  DLNode* phead = DLInit();
  DLPushBack(phead, 1);
  DLPushBack(phead, 2);
  DLPushBack(phead, 3);
  DLPushBack(phead, 4);
  DLPushBack(phead, 5);
  DLPrint(phead);
  printf("元素个数:%d\n", DLSize(phead));
  DLPopBack(phead);
  DLPopBack(phead);
  DLPopBack(phead);
  DLPopBack(phead);
  DLPrint(phead);
  if (DLEmpty(phead))
    printf("空表\n");
  DLPopBack(phead);
  DLPrint(phead);
  if (DLEmpty(phead))
    printf("空表\n");
  DLDestory(phead);
  phead = NULL;
}
int main()
{
  test5();
  return 0;
}

好了,对带头双向循环链表的讲解就到这里,欢迎大家指正!!!

da0d28ea57214b269b7cd75d68be82e5.png


目录
相关文章
|
2月前
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
67 4
|
3月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
109 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
5天前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
23 5
|
19天前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
80 5
|
2月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
130 4
|
2月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
2月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
58 0
|
3月前
|
存储 Java
数据结构第三篇【链表的相关知识点一及在线OJ习题】
数据结构第三篇【链表的相关知识点一及在线OJ习题】
37 7