数据结构-单链表

简介: 数据结构-单链表

在学习链表之前,我们先说说之前学的顺序表,在前面的章节中,我们说,顺序表实际上是对数组的操纵,它的内存空间是连续的,可以通过数组下标随机访问,但是链表就不行,这是顺序表的优点,那它有没有缺点呢?

答案是有的。

顺序表的问题:

1. 中间/头部的插入删除,时间复杂度是O(N)。

2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小消耗。

3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容      到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

如何解决上述问题呢?下面我们来学习链表。

1. 链表

1.1 链表的概念和结构

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

顺序表的问题本质就在于,它是一块连续的物理空间,而链表就不一样了。链表在内存中申请的是一块一块的内存空间,相互之间通过节点联系在一起。

开始时给一个指针指向第一个空间,然后通过节点找到第二个,第二个找到第三个........最后一个节点指向空(NULL)

如下图:

现在我们首先来定义一个结构体:

typedef int SLDataType;
typedef struct SListNode
{
  SLDataType data;
  struct SListNode* next;
}SLTNode;

结构体中有一个数据data,还有一个结构体指针next。

下面我们简单来遍历打印一个链表:

void SLDPrint(SLTNode* phead)
{
  SLTNode* cur = phead;
  while (cur != NULL)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}

上述代码中,把头节点赋给cur,cur->data是结构体中的数据,代码中的cur=cur->next刚开始可能有点难以理解,实际上cur->next访问的是存放在结构体中的下一个结构体的地址,->相当于对其进行解引用,这样cur就可以找到下一个节点,然后打印下一个结构体的数据,直到最后一个节点的cur->next指向空指针(NULL)。

上图其实就是链表的逻辑结构,这是为了方便形象理解,想象出来的。实际上链表在内存中是下面的物理结构

实际的物理结构,变量phead中存放的是第1个节点的地址,第1个节点的结构体中存放的是第2个节点的地址, 2中存放3节点的地址,3中存放4节点的地址.......直到最后的节点中存放的是空。

这样上述代码中的cur = cur->next就更好理解了,它实际上相当把next中的地址拷贝到cur中去,然后cur就指向了下一个节点。

1.2 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向或者双向:

2. 带头或者不带头

3. 循环或者非循环

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

2. 单链表的实现:

单链表头插数据:

头插数据只需要使新开辟出来的newnode节点下一个指向的是头节点phead,然后把头节点指向在newnode的地方。

下面我们来看看下面代码实现:

//单链表头插数据
void SLPushFront(SLTNode* phead, SLDataType x)
{
  //动态内存开辟
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc");
    return;
  }
  newnode->data = x;
  newnode->next = NULL;
    //头插
  newnode->next = phead;
  phead = newnode;
}

问题来了,上述代码正确吗?

看起来逻辑十分严密是吧,但是我们测试运行一下就会发现,什么都没有打印出来:

这又是为什么呢?

因为我们在头插的时候改变的是指针,例如:newnode->next = phead; phead = newnode;改变指针实际上就是,把指针的值拷贝过去,当我们传plist,并用phead接收时,实际上就是phead把plist的值拷贝过去,但是后面phead = newnode,又把newnode的值拷贝过去了,原有的值被覆盖了,plist和phead建立不上联系。

那该怎么办呢?

很简单,既然我们要改变指针,那就传指针的地址,用二级指针接收,使用时解引用就行。通过*phead=newnode可以使newnode和plist直接建立联系。

代码修改如下:

//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
  //动态内存开辟
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc");
    return;
  }
  newnode->data = x;
  newnode->next = NULL;
  newnode->next = *phead;
  *phead = newnode;
}

测试:

单链表尾插数据

尾插数据,因为链表不是连续物理空间,所以我们要先找到链表的尾,然后再指向新开辟的节点newnode,并把newnode->next置为NULL。

同理,尾部插入也需要使用二级指针,如果不使用二级指针,在phead=newnode这条语句中,因为此时的newnode和phead是局部变量,出了作用域就销毁了,所以phead和plist建立不上联系。而如果我们用二级指针,*phead=newnode,相当于把newnode的值直接给plist,尽管phead和newnode后面还会销毁,但是新节点和plist的联系已经建立起来了。

不论是头插还是尾插数据,都要开辟新空间,所以我们可以将其封装为一个函数:

//动态内存开辟
SLTNode*CheckSList(SLDataType x)
{
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc");
    return;
  }
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}

尾插数据代码:

//单链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x)
{
  SLTNode* newnode = CheckSList(x);
  //1.空链表
  //2.非空链表
  if (*phead == NULL)
  {
    *phead = newnode;
  }
  else
  {
    SLTNode* tail = *phead;
    //找到尾
    while (tail->next != NULL)
    {
      tail = tail->next;
    }
    //尾插
    tail->next = newnode;
  }
}

注意:要判断链表是不是空链表。

测试:

单链表头删数据:

要想头删数据,只需要将头节点指向第二个节点,并将头节点free掉,置为NULL。

注意:分三种情况,无节点一个节点多个节点。实现的方式不同,需要判断。

//单链表头删数据
void SLPopFront(SLTNode** phead)
{
  assert(*phead);
  //一个节点
  //多个节点
  if ((*phead)->next == NULL)
  {
    free(*phead);
    *phead = NULL;
  }
  else
  {
    SLTNode* start = *phead;
    *phead = start->next;
    free(start);
    start = NULL;
  }
}

测试:

单链表尾删数据:

要想尾删数据,可以先找到倒数第二个节点,把它后面的节点free掉,并置为NULL即可。

注意:分三种情况,无节点一个节点多个节点。实现的方式不同,需要判断。

代码如下:

//单链表尾删数据
void SLP0pBack(SLTNode** phead)
{
  assert(*phead);
  //一个节点
  //多个节点
  if ((*phead)->next == NULL)
  {
    free(*phead);
    *phead = NULL;
  }
  else
  {
    SLTNode* tail = *phead;
    //找到倒数第二个节点
    while (tail->next->next != NULL)
    {
      tail = tail->next;
    }
    free(tail->next);
    tail->next = NULL;
  }
}

测试:

单链表查找并修改数据:

查找数据很简单,直接把链表遍历一遍就行。而查找和修改可以同时实现。

直接上代码:

//单链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x)
{
  assert(phead);
  SLTNode* cur = phead;
  int count = 0;
  while (cur != NULL)
  {
    if (cur->data == x)
    {
      return cur;
    }
      cur = cur->next;
  }
  return NULL;
}

上述代码可以同时实现查找和修改,查找到数据后,返回节点指针,通过指针修改结构体数据:

SLTNode* pos = SLFind(plist,2);
  if (pos != NULL)
    pos->data = 30;

测试:

在上文中,我们发现有些参数传的是一级指针,有些传的是二级指针,为什么呢?

其实给打印和查找函数传的就是一级指针,而其他的头插、尾插、头删、尾删都是二级指针,原因很明显,打印和查找函数不需要改变指针,而其他的需要改变指针,要改变指针就要使用二级指针。

下面我们再来补充一个内容,

什么时候需要断言?

比如:查找函数需要断言assert(phead)吗?

不需要,因为即使phead传过来的是空链表,查找函数也可以找,找不到就返回NULL。同理,打印函数也不需要断言,它可以打印空链表NULL。

头插函数中需要断言assert(phead)

需要,因为phead是头指针plist的地址,永远都不能为空,一旦为空,再对其解引用就成空指针NULL了。

头插函数中需要断言assert(*phead)

不需要,因为它即使为空,空链表也可以插入数据啊。

头插函数断言如下:

//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
  assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
  //assert(*phead);不需要断言,链表为空,也需要能插入
  SLTNode*newnode=CheckSList(x);
  newnode->next = *phead;
  *phead = newnode;
}

头删函数中需要断言assert(phead)assert(*phead)吗?

都需要,头删函数中,一旦链表为空就不能再删了,所以需要断言assert(*phead),而断言assert(phead)的原因和上文一致。

头删函数断言如下:

//单链表头删数据
void SLPopFront(SLTNode** phead)
{
  assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
  assert(*phead);//链表为空,不能再删。
  //一个节点
  //多个节点
  if ((*phead)->next == NULL)
  {
    free(*phead);
    *phead = NULL;
  }
  else
  {
    SLTNode* start = *phead;
    *phead = start->next;
    free(start);
    start = NULL;
  }
}

同理,尾删和头删保持一致,尾插和头插保持一致。

下面我们接着讲链表任意位置的插入和删除

单链表在pos之前插入数据:

要想在pos之前插入数据,就要先找到pos前一个节点,然后把使该节点下一个指向newnode,让newnode的下个节点指向pos。

代码如下:

//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x)
{
  assert(phead);
  assert(pos);
  if (*phead == pos)
  {
    SLPushFront(phead, x);
  }
  else
  {
    SLTNode* newnode = CheckSList(x);
    SLTNode* cur = *phead;
    while (cur->next != pos)
    {
      cur = cur->next;
    }
    cur->next = newnode;
    newnode->next = pos;
  }
}

注意:上述代码中复用了头插函数,因为当pos在头节点的位置时,while循环就找不到pos的前一个节点了,所以这种情况需要单独判断,当pos在头节点位置时,直接使用头插。

测试:先用查找函数找到pos,然后在pos前面插入数据:

单链表在pos之后插入数据:

在pos之后插入数据很简单,直接插入就行。

上代码:

//单链表在pos后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x)
{
  assert(phead);
  SLTNode* newnode = CheckSList(x);
  SLTNode* next = pos->next;
  pos->next = newnode;
  newnode->next = next;
}

测试:也是先用查找函数找到pos,然后在pos后面插入数据:

单链表在pos位置删除数据:

经过上文的练习,这个应该很简单吧。直接找到pos前一个节点,让它指向pos下一个节点,然后将pos节点free就行。

上代码:

//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos)
{
  assert(phead);
  assert(*phead);
  if (*phead == pos)
  {
    SLPopFront(phead);
  }
  else
  {
    SLTNode* prev =*phead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
  }
}

测试:

至于在pos之前和之后删除数据,和前面的思路一样,不再赘述。

3.完整代码:

test.c:

仅供测试用例。

#define  _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//测试函数
void test1()
{
  SLTNode* plist = NULL;
  SLPushFront(&plist, 1);
  SLPushFront(&plist, 2);
  SLPushFront(&plist, 3);
  SLPushFront(&plist, 4);
  SLTNode* pos = SLFind(plist, 2);
  if (pos != NULL)
  SLPErase(&plist, pos);
  /*SLPushBack(&plist, 5);
  SLPushBack(&plist, 6);
  SLPushBack(&plist, 7);*/
  /*SLTNode* pos = SLFind(plist,2);
  if (pos != NULL)
    pos->data = 30;*/
  /*SLPopFront(&plist);
  SLPopFront(&plist);
  SLPopFront(&plist);*/
  SLPrint(plist);
}
int main()
{
  test1();
  return 0;
}

SList.h:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义结构体
typedef int SLDataType;
typedef struct SListNode
{
  SLDataType data;
  struct SListNode* next;
}SLTNode;
//打印函数
void SLPrint(SLTNode*phead);
//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x);
//单链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x);
//单链表头删数据
void SLPopFront(SLTNode** phead);
//单链表尾删数据
void SLP0pBack(SLTNode** phead);
//单链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x);
//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x);
//单链表在pos之后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x);
//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos);

SList.c:

#define  _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void SLPrint(SLTNode* phead)
{
  SLTNode* cur = phead;
  while (cur != NULL)
  {
    printf("%d->", cur->data);
    cur = cur->next;
  }
  printf("NULL\n");
}
//动态内存开辟
SLTNode*CheckSList(SLDataType x)
{
  SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
  if (newnode == NULL)
  {
    perror("malloc");
    return NULL;
  }
  newnode->data = x;
  newnode->next = NULL;
  return newnode;
}
//链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
  assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
  //assert(*phead);不需要断言,链表为空,也需要能插入
  SLTNode*newnode=CheckSList(x);
  newnode->next = *phead;
  *phead = newnode;
}
//链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x)
{
  assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
  //assert(*phead);不需要断言,链表为空,也需要能插入
  SLTNode* newnode = CheckSList(x);
  //1.空链表
  //2.非空链表
  if (*phead == NULL)
  {
    *phead = newnode;
  }
  else
  {
    SLTNode* tail = *phead;
    //找到尾
    while (tail->next != NULL)
    {
      tail = tail->next;
    }
    //尾插
    tail->next = newnode;
  }
}
//链表头删数据
void SLPopFront(SLTNode** phead)
{
  assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
  assert(*phead);//链表为空,不能再删。
  //一个节点
  //多个节点
  if ((*phead)->next == NULL)
  {
    free(*phead);
    *phead = NULL;
  }
  else
  {
    SLTNode* start = *phead;
    *phead = start->next;
    free(start);
    start = NULL;
  }
}
//链表尾删数据
void SLP0pBack(SLTNode** phead)
{
  assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
  assert(*phead);//链表为空,不能再删。
  //一个节点
  //多个节点
  if ((*phead)->next == NULL)
  {
    free(*phead);
    *phead = NULL;
  }
  else
  {
    SLTNode* tail = *phead;
    //找到倒数第二个节点
    while (tail->next->next != NULL)
    {
      tail = tail->next;
    }
    free(tail->next);
    tail->next = NULL;
  }
}
//链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x)
{
  assert(phead);
  SLTNode* cur = phead;
  int count = 0;
  while (cur != NULL)
  {
    if (cur->data == x)
    {
      return cur;
    }
      cur = cur->next;
  }
  return NULL;
}
//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x)
{
  assert(phead);
  assert(pos);
  if (*phead == pos)
  {
    SLPushFront(phead, x);
  }
  else
  {
    SLTNode* newnode = CheckSList(x);
    SLTNode* cur = *phead;
    while (cur->next != pos)
    {
      cur = cur->next;
    }
    cur->next = newnode;
    newnode->next = pos;
  }
}
//单链表在pos后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x)
{
  assert(phead);
  SLTNode* newnode = CheckSList(x);
  SLTNode* next = pos->next;
  pos->next = newnode;
  newnode->next=next;
}
//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos)
{
  assert(phead);
  assert(*phead);
  if (*phead == pos)
  {
    SLPopFront(phead);
  }
  else
  {
    SLTNode* prev =*phead;
    while (prev->next != pos)
    {
      prev = prev->next;
    }
    prev->next = pos->next;
    free(pos);
  }
}

以上就是今天学习的关于单链表的所有内容。

未完待续。。。

目录
相关文章
|
4月前
【数据结构】单链表(长期维护)(1)
【数据结构】单链表(长期维护)(1)
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
48 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
2月前
|
存储
[数据结构] -- 单链表
[数据结构] -- 单链表
26 1
|
3月前
|
存储 Java
java数据结构,线性表链式存储(单链表)的实现
文章讲解了单链表的基本概念和Java实现,包括头指针、尾节点和节点结构。提供了实现代码,包括数据结构、接口定义和具体实现类。通过测试代码演示了单链表的基本操作,如添加、删除、更新和查找元素,并总结了操作的时间复杂度。
java数据结构,线性表链式存储(单链表)的实现
|
2月前
|
存储
【数据结构】——单链表实现
【数据结构】——单链表实现
|
2月前
|
存储
数据结构2——单链表
数据结构2——单链表
39 1
|
2月前
|
存储
【初阶数据结构】深入解析单链表:探索底层逻辑(无头单向非循环链表)(一)
【初阶数据结构】深入解析单链表:探索底层逻辑(无头单向非循环链表)
|
2月前
|
存储
数据结构(单链表)
数据结构(单链表)
22 0
|
2月前
|
存储
数据结构--单链表
数据结构--单链表
|
3月前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
694 6