数据结构杂谈(三)

本文的所有代码均由C++编写

如果你已经看完这篇杂谈,你可以前往上一篇杂谈→数据结构杂谈(二)_尘鱼好美的小屋-CSDN博客

3 单链表


文章目录

  • 3 单链表
    • @[toc]
    • 3.1 单链表的定义
      • 3.1.1 引入
      • 2.1.2 单链表和顺序表的优劣
      • 2.1.3 单链表的代码定义
    • 3.2 单链表的初始化
      • 3.2.1 不带头结点的单链表
      • 3.2.2 带头结点的单链表
    • 3.3 单链表的插入
      • 3.3.1 带头结点的单链表插入
      • 3.3.2 不带头结点的单链表插入
      • 3.3.3 指定结点后插操作
      • 3.3.4 指定结点前插操作
    • 3.4 单链表的删除
      • 3.4.1 带头结点的单链表删除
      • 3.4.2 指定结点的删除
    • 3.5 单链表的查找
      • 3.5.1 按位查找
      • 3.5.2 按值查找
      • 3.5.3 求单链表长度
    • 3.6 单链表的建立
      • 3.6.1 尾插法
      • 3.6.2 头插法
    • 3.7 补充算法
      • 3.7.1 单链表的销毁
      • 3.7.2 清空链表

3.1 单链表的定义

3.1.1 引入

线性表有两种,第一个是我们前面讲到的顺序表,对应顺序存储。第二个是链表,对应链式存储

物理结构逻辑结构
顺序表顺序存储
链表链式存储

要谈论链表,我们就要先谈最简单的链表;所以在这里首先要提出一个单链表的概念。单链表也叫线性链表

线性表的链式存储结构是用一组任意的存储单元存储线性表的数据元素。由图可知,每个数据元素aia_iai由两部分组成,一部分放数据元素信息,我们叫做数据域;另外一部分放下一个数据元素地址的信息,我们叫指针域,两部分加起来合称为结点。指针域里面放的地址我们叫指针或者。n个结点结成一个链表。因为每个结点只放了一个指针域,所以我们又叫单链表或线性链表。

image-20220120094309585

2.1.2 单链表和顺序表的优劣

顺序表优缺点单链表优缺点
顺序表优点:可随机存取,存储密度高单链表优点:不要求大片连续空间,改变容量方便
缺点:要求大片连续空间,改变容量不方便。缺点:不可随机存取,要耗费一定空间存放指针。

2.1.3 单链表的代码定义

在前面我们说过单链表中的每个节点都包含一个数据域和一个指针域,所以在C++中我们常常用结构体的方式去定义某个节点。

//定义一个结点
typedef struct LNode{		//定义单链表结点类型ElemType data; //每个节点存放一个数据元素struct LNode *next; //指针指向下一个结点
}LNode*, LinkList;

需要注意的是:在这里我们可以看到指针域用了一个结构体嵌套,这是因为下一个节点也是结构体,它的地址也会是结构体指针类型。

在上面的定义中,我们还使用了数据类型重命名typedef重命名了两个名字,其中LNode*主要是强调他是一个结点,而LinkList主要强调这个结点为整个链表;用了两种命名是为了代码的可读性更强。】

对于拥有多个类型的数据元素,我们常常采用嵌套结构体的方式;先将数据存放某一个结构体,然后再将该结构体放入定义结点的结构体中,举例如下:

typedef Struct{char num[8];char name[8];int score;
}ElemType;typedef struct LNode{ElemType data;//数据域struct LNode *next;//指针域
}LNode,*LinkList;

3.2 单链表的初始化

对于链表来说是需要初始化的,这是因为结点中可能含有脏数据。对于单链表初始化即构造一个空表。

这里我们要分为两类情况,一类是不带头结点的单链表,一类是带头结点的单链表。下面先说说两者的区别:

  1. 所有的链表都要有个头指针first,带头结点的链表的头指针指向的是头结点,头结点的指针域指向首元结点,不带头结点的头指针直接指向首元结点。
  2. 两者在操作上有区别:在删除和插入操作中,无论删除和插入的位置如何,带头结点的链表不需要修改头指针的值,而不带头结点的有时候需要。在清空操作中,带头结点的保留头结点,而不带头结点的要销毁。.
  3. 在结构上,带头结点的单链表,不管链表是否为空,均含有一个头结点,不带头结点的单链表不含头结点。
  4. 在操作上,带头结点的单链表的初始化为申请一个头结点。无论插入或删除的位置是地第一个结点还是其他结点,算法步骤都相同。不带头结点的单链表,其算法步骤要分别考虑插入或删除的位置是第一个结点还是其他结点。

3.2.1 不带头结点的单链表

由于单链表不带头结点,这就导致了如果初始化表,那就是表全为空。其代码定义如下:

#include <iostream>
using namespace std;//定义链表(不带头结点)
typedef struct  
{//数据域int data;//指针域struct LNode* next;
}LNode,* LinkList;//初始化链表
bool InitList(LinkList& L)
{L = NULL;return true;
}int main()
{LinkList L;InitList(L);
}

此时如果要判断单链表是否为空,只需单纯判断L是否为空即可。

//判断单链表是否为空
bool Empty(LinkList L)
{if (L == NULL)return true;elsereturn false;
}

3.2.2 带头结点的单链表

  1. 生成新结点作头结点,用头指针L指向头结点。
  2. 将头结点的指针域置空,防止内存中有遗留的脏数据。
#include <iostream>
using namespace std;//定义链表(不带头结点)
typedef struct LNode
{//数据域int data;//指针域struct LNode* next;
}LNode,* LinkList;//初始化链表
bool InitList(LinkList& L)
{L = new LNode;if (L == NULL)//这里为了防止申请内存不足return false;L->next = NULL;return true;
}int main()
{LinkList L;InitList(L);
}

此时如果想判断单链表是否为空,只需判断头结点中储存的指针域是否为空即可。

//判断单链表是否为空
bool Empty(LinkList L)
{if (L->next == NULL)return true;elsereturn false;
}

一般来说,我们写的代码都是带头节点的,用过都说好。

3.3 单链表的插入

在下面的基本操作中,我们需要知道一些比较特殊的步骤,有个这些步骤,即使不看源码,你也能写出类似的代码。

p = L;//p指向头结点
s = L->next;//s指向首元结点
p = p->next;//p指向下一结点

这里还要多插一句:为了让我们的代码更具健壮性,我们应该多考虑极端情况;为了避免重复代码,使我们的代码简洁易维护,我们应该把基本操作封装成一个函数。

3.3.1 带头结点的单链表插入

单链表插入原理图如下:

image-20220201151552276

以下给出代码实现:

ListInsert(&L,I,e):插入操作。在表L中的第i个位置上插入指定元素e。

//按位插入
bool ListInsert(LinkList& L, int i, int e)
{if (i < 1)return false;LNode* p;//生成指针p,用于指向插入端的前一个结点int j = 0;//用于扫描计数p = L;//p初始化位置指向头结点//移动p到插入位置的前一个结点while (p!=NULL && j< i-1){p = p->next;j++;}//p不能移出链表之外if (p == NULL)return false;//生成要插入的新结点LNode* s = new LNode;s->data = e;s->next = p->next;p->next = s;return true;
}

需要注意的是:s->next = p->nextp->next = s这两句不可颠倒,否则链表的后半部分将会丢失。还有添加结点必须一个一个添加,不能说第二个还没添加就添加第三个。

3.3.2 不带头结点的单链表插入

实际上,不带头结点的单链表插入原理和带头结点的差不多,只是在第1个位置需要做特殊处理,因为头指针指着第一个元素。为此,我们要在带头结点的单链表插入代码中添加如下代码:

if (i == 1){LNode* s = new LNode;s->data = e;s->next = L;L = s;return true;}

不带头结点写代码很不方便,推荐用带头结点;而在考研中,带头结点和不带头结点的情况均有可能考查,要注意审题。

3.3.3 指定结点后插操作

对于某一个结点,我们想要在其后插入一个新节点,代码如下:

bool InsertNextNode(LNode* p, int e)
{if (p == NULL)return false;LNode* s = new LNode;//内存不足判断if (s == NULL)return false;s->data = e;s->next = p->next;p->next = s;return true;
}

3.3.4 指定结点前插操作

对于后插来说,实际上根据指定的结点是可以找到下一个结点的。可以对于前插来说,指定的结点是不能找到前一个结点的,这是因为结点中只存放了下一个结点的指针,而没有存放上一个结点的指针域。

那么如何解决这个问题呢?我们可以用后插模仿成前插,什么意思呢?意思就是后插一个结点,然后把前一个结点的数据拷贝到后一个结点,然后对前一个结点赋值即可做成前插的效果。而且这种思路的时间复杂度为O(1),用了都说好。代码实现如下:

bool InsertPriorNode(LNode* p, int e)
{if (p == NULL)return false;LNode* s = new LNode;if (s == NULL)return false;s->next = p->next;p->next = s;s->data = p->data; p->data = e;return true;
}

3.4 单链表的删除

3.4.1 带头结点的单链表删除

bool ListDelete(LinkList& L, int i, int& e)
{if (i < 1) //索引处于链表之外return false;LNode* p;int j = 0;p = L;while (p != NULL && j < i - 1){p = p->next;j++;}if (p == NULL)//索引处于链表右边之外return false;if (p->next == NULL)return false;LNode* q = p->next;e = q->data;p->next = q->next;delete(q);return true;
}

3.4.2 指定结点的删除

//删除指定结点
bool DeleteNode(LNode* p)
{if (p == NULL)return false;LNode* q = p->next;p->data = p->next->data;p->next = q->next;delete(q);return true;
}

实际上,上面提供的代码具有BUG,因为如果p是最后一个结点,那么p->next是空值,无法提供data,这个时候只能从表头开始依次寻找p的前驱,时间复杂度为O(n)。

3.5 单链表的查找

3.5.1 按位查找

实际上对于按位查找,无非就是从头找到尾,直到找到第i个元素位置。由于这个算法的时间复杂度取决于i的位置,所以有最好情况最坏情况

//按位查找
LNode* GetElem(LinkList L, int i)
{if (i < 0)return NULL;LNode* p;p = L;int j = 0;while (p != NULL && j < i){p = p->next;j++;}return p;
}

分析算法时,要时刻分析极端情况,如图所示:

image-20220129140243130

  • 由于有头结点,头结点不允许查找,而其为0号位,所以i至少要为1。
  • 由于如果开辟内存不足,那么ana_nan可以为NULL,此时i要小于ana_nan

还要一个需要注意的点,你是否发现这段按位查找的代码好像似曾相识?没错,这段代码出现在插入和删除操作中,所以当你把按位查找封装成一个函数(基本操作)的时候,写插入基本操作就无需再写一次代码,直接调用按位查找函数即可,这样可提高代码复用性。

3.5.2 按值查找

//按值查找
LNode* LocateElem(LinkList& L, int e)
{LNode* p = L->next;while (p != NULL && p->data != e)p = p->next;return p;
}

【注:该算法时间复杂度为O(n)。】

3.5.3 求单链表长度

//求单链表长度
int Length(LinkList L)
{int len = 0;LNode* p = L; //初始化指针于头结点位置while (p->next != NULL){p = p->next;len++;}return len;
}

3.6 单链表的建立

如果给你很多个数据元素,要把它们存到一个单链表里边,怎么达到目的呢?

3.6.1 尾插法

尾插法没什么好讲的,利用我们前面所讲的定义、初始化、插入,即可完成尾插法,需要注意的是,每次插入一个元素,需要新指定一个变量length来统计表的长度。

但是如果使用普通的遍历插入,每次插入都会涉及到while循环,时间复杂度为O(n2)O(n^2)O(n2),这样的算法明显太垃圾了。

于是我们又思考前面在学习插入的时候,我们使用过一个叫做指定结点的后插操作。其步骤如下:

  1. 从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
  2. 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。

为了更直观地看懂这个过程,我特意花了个图:

image-20220129152806386

代码示例如下:

//正位序输入n个元素的值,建立带表头结点的单链表L
void CreateList_R(LinkList &L,int n){L = new LNode;L->next = NULL;r = L; //尾指针r指向头结点for(i = 0;i<n;i++){p = new LNode;cin>>p->data; //生成新结点,输入元素值p->next = NULL;r->next = p; //插入到表尾r= p; //r指向新的尾结点}
}//CreateList_R

【注:这里尾插法的时间复杂度是O(n)】

3.6.2 头插法

头插法也很好理解,本质是插入的位置一直处于头结点之后,且使用指定结点的后插操作。其基本步骤如下:

  1. 从一个空表开始重复读入数据
  2. 生成新结点,将读入数据存放到新结点的数据域中
  3. 从最后一个结点开始,依次将各结点插入到链表的前端

对于头插法我也画了个图:

image-20220129151919386

其代码示例如下:

void CreateList_H(LinkList &L,int n){L = new LNode;L->next = NULL;for(i = n;i>0;i--){p = new LNode;cin>>p->data;p->next = L->next;L->next = p;}
}//CreateList_H

【注:这里头插法的算法时间复杂度是O(n)】

需要注意的是,这里还能应用链表的逆置,用指针扫描某一个某一个链表后,利用头插法插到另外一个链表,即可实现逆置。

3.7 补充算法

3.7.1 单链表的销毁

销毁:链表销毁后不存在

【算法思路】从头指针开始,依次释放所有结点

image-20211020152351152

我们销毁的思路是:我们还需要另外一个指针变量P,这个指针变量用于结点的操作。若想实现变量P对某结点的操作,首要任务就是让P指向该结点,即把该结点的地址赋给P。那该节点的地址存于头指针L,所以只需p = L即可。当然,当P = L后,不能立马删除p,否则L丢失,链表也跟着丢失;所以我们需要在P = L后,把L移到下一个结点,即L = L->next,然后再释放P(free§)即可。循环上述操作,即可删除链表。


【算法描述】

Status DestroyList_L(LinkList &L){//销毁单链表LLNode *p;while(L){p = L;L = L->next;delete(p);}return Ok;
}

3.7.2 清空链表

清空链表:链表仍然存在,但链表中无元素,成为空链表(头指针和头结点仍然在)

【算法思路】依次释放所有结点,并将头结点指针域设置为空。

image-20211020163023378

先将头指针的指针域赋给指针变量p,这样的话,p就定位了要删除的结点了,但是如果现在直接删除,那么后面的链表就会丢失了。所以这时候我们引入第三个指针变量q,q来保证后面的链表不丢失,当我们q移到p要删除结点的下一个结点后,即q = p->next,我们再去释放p,即delete§。直到清空列表为止。


【算法描述】

Status ClearList(LinkList &L){LNode *p,*q;p = L->next;while(p){p=q->next;free(p);p = q;}L->next = NULL; //头结点指针域为空return OK;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/398870.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mac Book Pro不能识别移动硬盘

2019独角兽企业重金招聘Python工程师标准>>> 今天把一台Mac Book Pro重装了一下系统&#xff0c;然后想把移动硬盘上的资料考入电脑&#xff0c;发现插上移动硬盘电脑没有反映。马上搜索解决办法&#xff0c;众说纷纭。因为没重装之前是好的&#xff0c;所以判断为软…

数据库杂谈(四)——关系演算和E-R数据模型

文章目录3 形式化关系查询语言3.2 关系演算和E-R数据模型3.2.1 概述3.2.2 元组关系演算3.2.2.1 概述3.2.2.2 形式化定义3.2.2.3 表达式的安全性3.2.3 域关系演算3.2.4 关于关系演算的习题3.2.5 对传统数据模型的评价3.2.6 E-R数据模型3.2.6.1 基本概念3.2.6.2 E-R图3.2.7 题型总…

小程序获取微信用户绑定的手机号

小程序API内提供getPhoneNumber借口来获取用户已经绑定到微信的手机号,但需要用户主动触发才能发起获取手机号接口&#xff0c;所以该功能不由 API 来调用; 基本流程如下: 使用方法 需要将 <button> 组件 open-type 的值设置为 getPhoneNumber&#xff0c;当用户点击并同…

华为正式发布鸿蒙多久可以用,华为正式发布鸿蒙OS,手机随时能用

8月9日下午&#xff0c;华为在松山湖召开了HDC2019开发者大会&#xff0c;而在此次大会上华为正式发布了传说中的华为鸿蒙系统&#xff0c;也就是Harmony OS。鸿蒙系统它是一个基于微内核的全新全长近分布式的操作系统&#xff0c;它不仅仅局限于某一个设备使用而是可以在其智慧…

WCF rest 的帮助页面和缓存机制

打开vs2010 在工具选项中选择扩展管理器——联机库 安装WCF REST Service Template 40(CS)模板 安装后新建wcf服务应用程序&#xff0c;删除默认建立的文件。 新建RestWcf4HelpPage.svc&#xff0c;代码如下 using System; using System.Collections.Generic; using System.Lin…

数据结构杂谈番外篇——时间复杂度计算

我们先给出推导的方法&#xff0c;然后下面一步一步来推导。 推导大O阶 用常数1取代运行时间中的所有加法常数在修改后的运行次数函数中&#xff0c;只保留最高阶项如果最高阶存在且不是1&#xff0c;则去除这个项相乘的常数所得结果即为大O阶 示例 int sum 0&#xff0c;…

Java学习笔记(二):String

String 在Java中String是作为引用对象存在的一种数据类型&#xff0c;用来保存字符串。 实例化和赋值 //直接声明 String s1 "Hello world!"; //通过构造函数创建, 提供了 11 种不同参数创建的方法 char[] c { h, e, l, l, o, .}; String s2 new String(c); Strin…

数据结构杂谈(一)

在你点进来这里的一瞬间&#xff0c;欢迎你找到了宝藏 这是一些关于数据结构和算法里最详细的阐述和学习心得&#xff0c;我十分乐意和你分享这些知识。 如果你已经看完这篇杂谈&#xff0c;可以前往下一篇→数据结构杂谈&#xff08;二&#xff09;_尘鱼好美的小屋-CSDN博客 1…

计算器排html页面,htmltest~计算器界面的实现

通过divcss和部分布局的作用&#xff0c;实现了&#xff0c;如下效果的计算器展示页面![C%]R$IGDK4J(%3LJXD3]SY.png&#xff0c;废话不多说&#xff0c;最主要的还是分享代码&#xff1a;html&#xff1a;Title.计算器-口 X编辑(E)查看(V)帮助(H)BackspaceCECMC789/sqrtMR456*…

win7 git 添加 ssh key

分两步走&#xff0c;借助git bash客户端&#xff1a;第一步&#xff1a;生成秘钥创建SSH key. 在用户主目录下&#xff0c; 看看有没有.ssh目录&#xff0c; 如果有&#xff0c; 再看看这个目录下有没有id_rsa 和 id_rsa.pub 这两个文件&#xff0c; 如果已经有了&#xff0c;…

解决三星手机EditText背景色的问题

问题描述android:background"#ffffff"其他手机手机背景都是白色&#xff0c;三星却是黑色的。怎么办 解决方案1写个主题测试下 解决方案2检查是三星的手机换背景色解决方案3你自己设置下edittext的背景就好了 解决方案4三星手机没这bug&#xff0c;你的固件不是正常的…

操作系统随笔(二)

如果你还没有读过第一篇随笔&#xff0c;请点击这里→操作系统随笔&#xff08;一&#xff09; 文章目录[toc]2 进程和线程2.1 进程2.1.1 进程模型2.1.2 进程的创建2.1.3 进程的终止2.1.4 进程的层次结构2.1.5 进程的状态2.1.6 进程的实现2.2 线程2.2.1 进程的使用2.2.2 经典的…

golang实现常用数据结构

2019独角兽企业重金招聘Python工程师标准>>> 1.数组栈的实现 package mainimport ( "fmt")type ItemType inttype Stack struct { node [10]ItemType maxsize int top int}func StackInit(p *Stack) { p.top -1}func StackCheckFull(p *Stack) bo…

【问题解决】无法创建新的堆栈防护页面

【问题发现】 项目中需要几个自定义的控件,菜鸟D定义了一个接口,打算使用多态来统一调用。在完成两个自定义控件后&#xff0c;项目都能正常运行。但是在第三个控件使用的时候就出了问题&#xff1a;将控件拖到界面上以后&#xff0c;不能拖动改变控件的宽度&#xff0c;一拖动…

java及python调用RabbitMQ

1,python调用MQ发送消息(生产者),话不多说,直接上干货 import pika 如下图 2.java调用MQ发送消息(生产者) 具体代码如下: python 的代码如下 connection pika.BlockingConnection(pika.ConnectionParameters(IP,端口,/))channel connection.channel()msg_props pika.Bas…

html知识收集(一)

1.页内跳转iframe <ul> <li><a href"1.html" target"iframe1">1</a></li> <li><a href"2.html" target"iframe1">2</a></li> </ul> </ul> <iframe name"i…

设定Word段落的背景色

段落背景不同于文字区别。很多新接触word的朋友都找不到怎么弄。 先把光标停留在需要设置的段落文字上&#xff0c;或者选择需要设置的段落文字。 点击段落里的边框和底纹&#xff0c;如图 在弹出框中选择底纹。 选择需要填充的颜色&#xff0c;在左下角处把文字切换成段落。…

Hadoop随笔(二)

文章目录[toc]2 Hadoop概念2.1 Hadoop简介2.2 MapReduce编程模型简介2.3 Hadoop系统的组成2 Hadoop概念 面对大数据量的处理需求&#xff0c;一个常用的方法就是把它们分割成互不依赖的小份数据来分别计算处理&#xff0c;这就是所谓的单指令多数据流(SIMD)的数据计算模式。Ha…

HDU 5102 The K-th Distance

题意&#xff1a;给你n-1条边&#xff0c;然后没两个节点的距离按照递增的顺序&#xff0c;求出前k项的和。 官方题解&#xff1a; 把所有边(u,v) 以及(v,u)放入一个队列&#xff0c;队列每弹出一个元素(u,v)&#xff0c;对于所有与u相邻的点w&#xff0c;如果w!v&#xff0c;就…

spring AOP 之五:Spring MVC通过AOP切面编程来拦截controller

示例1&#xff1a;通过包路径及类名规则为应用增加切面 该示例是通过拦截所有com.dxz.web.aop包下的以Controller结尾的所有类的所有方法&#xff0c;在方法执行前后打印和记录日志到数据库。 新建一个springboot项目 1&#xff1a;首先定义maven <?xml version"1.0&q…