线性表之单向链表

2024-09-01 19:36
文章标签 链表 线性表 单向

本文主要是介绍线性表之单向链表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 单向链表的结构

        在上一个章节中为大家详细讲解了静态链表,它解决了插入和删除数据的时候大量移动元素的问题,但是解决不了合理分配和使用内存的问题。解决这个问题的最优方案就是使用动态链表,简称链表。

        链表和数组都可以称之为线性表,数组是一种顺序存储结构的线性表,而链表是一种链式存储结构的线性表,链表中的节点和节点之间的内存是不连续的,它们之间的前后关系需要通过指针来维系。

        关于链表有很多种,比如:单向链表、单向循环链表,双向链表、双向循环链表,并且这些链表可以带头结点也可以不带头结点。

1.1 单向链表节点

        我们先从单向链表说起,所谓的单向链表就是链表的节点中只有一个指针域,并且这个指针域指向当前节点的下一个节点(后继节点)的地址。

假设单向链表的节点存储的是整形数据,那么该节点的定义如下:

// 定义一个节点,此为 C++ 语法
struct Node
{int data = 0;Node* next = nullptr;
};

对于上图这个单向链表而言:

  • 链表第1个节点中的Next指针保存了第2个节点中Data1的起始地址
  • 链表第2个节点中的Next1指针保存了第3个节点中Data2的起始地址
  • 链表第3个节点中的Next2指针保存了第4个节点中Data3的起始地址
  • 链表最后一个节点中的Next3指针指向了空地址nullptr

通过这种方式我们就可以使用指针维护一个链式结构的线性表了。

1.2 头结点和头指针

1.2.1 头结点

        头结点是为了操作的方便和统一而设立的,放在第一个数据节点之前,其数据域一般没有意义(有时也用来存储链表的长度)。

  • 有了头结点之后,在第一个数据节点前插入新节点和删除第一个数据节点,其操作流程和其他数据节点无异
  • 链表可以有头结点,也可以没有头结点

下图是不带头结点的链表:

下图是带头结点的链表:

        通过对比,二者的区别一目了然,平时建议使用带头结点的链表,它的优势在后边的链表操作章节大家会有深刻体会。

        链表中的最后一个节点我们将其称之为尾节点,尾节点和其它节点的不同之处在于它的指针域指向的不是下一个数据节点的地址而是空,通常用 NULL(C语言)或者 nullptr(C++)表示。

1.2.2 头指针

        头指针顾名思义就是指向链表头结点地址的指针,对于链表的操作必须从头指针开始。在编码过程中一般都是通过头指针来辅助我们完成链表的节点插入、节点删除、节点遍历等操作。

  • 对于不带头结点的链表,头指针指向的是第一个数据节点的地址

  • 对于带头结点的链表,头指针指向的是头结点的地址

        在进行链表操作的时候定义一个指针让其指向链表的头结点,此时这个指针就是上面所说的头指针了。它是我们操作链表的过程中的一个必要步骤,那么如何对链表进行节点的添加、删除以及遍历呢?接下来我们来逐一分析。

2. 单向链表的操作

2.1 链表的遍历和搜索

关于链表的遍历应该是链表操作中最简单的操作了,主要步骤如下:

  • 定义一个辅助指针,让指针指向链表的第一个节点,得到头指针
  • 根据头结点的next域指针,访问第二个链表节点,再根据第二个节点的next域指针访问第三个链表节点,以此类推……
  • 判断如果某个链表节点的next域指针指向空(NUL或者nullptr),遍历结束

如果掌握了链表的遍历,想要搜索链表中的某个节点,大家也就有思路了,只需要在遍历链表过程中,对每个节点进行判断即可。

关于链表的搜索无外乎有两种方式:

  • 按照值搜索:遍历过程中将每个节点的值和要搜索的值进行比较
  • 按照节点搜索:遍历过程中将每个节点的地址和要搜索的节点的地址进行比较

2.2 链表的插入

2.2.1 带头结点的插入

场景1:在头部和中间位置插入新节点

        对于带头节点的链表而言,在头部插入节点就是把新的数据节点作为链表的第一个数据节点,它是头结点的后继节点。

        带头结点的单向链表在进行新节点插入的时候需要判断的情况相对较少,在链表中插入第一个数据节点和在中间位置插入数据节点的处理流程是一样的。

在链表的头部和中间位置(pos)插入新节点需要分以下几步:

  • 创建一个新的节点,并初始化,记作newNode
  • 遍历链表找到pos-1位置的节点,记作preNode
  • 将新节点的后继节点设置为pos位置的节点,也就是newNode->next = preNode->next
  • 重置preNode节点的后继,设置为newNode,即:preNode->next = newNode

温馨提示:第三步、第四步操作是不能颠倒的。

下图是将新节点插入到链表的非第一个数据节点的位置:

下图是将新节点作为第一个数据节点插入到链表中:

有图有真相,证明在以上两种情况下插入新节点的操作流程是相同的。

场景2:在尾部插入新节点

        在链表的尾部添加新节点就是让原来的尾节点指向新节点的地址,让新节点的指针域指向空地址。主要的操作步骤如下:

  • 遍历链表并找到它的尾节点,记作tailNode
  • 创建一个新的链表节点,记作newNode,并初始化,有两种方式:
    • newNode->next = nullptr
    • newNode->next = tailNode->next
  • 让找到的尾节点的指针域指向新创建的节点的地址,tailNode->next = newNode

        在链表尾部添加新节点的时候,上图中两条红色的线,先连接哪一条取决于 newNode 节点的 next 域的初始化方式(详见步骤2)。

2.3 链表的删除

2.3.1 带头结点的删除

场景1:删除头部和中间位置的节点

        对于带头结点的链表而言,所谓的删除头部节点指的就是删除链表中的第一个数据节点。

        删除带头结点的链表中的第一个数据节点和中间位置的数据节点的流程是一样的,不会出现链表中没有节点的情况。

删除头部和中间位置(pos)的节点的处理流程如下:

  • 遍历链表,搜索链表的节点,找到要删除的节点的上一个节点(pos-1),记作preNode
  • 通过preNode找到要删除的节点,记作delNode
    • delNode = preNode->next
  • 将delNode从链表中移除,即preNode->next = delNode->next
  • 释放delNode节点指向的内存资源

下图为删除链表第一个数据节点的示意图:

下图为删除链表中间位置的数据节点的示意图:

场景2:删除尾部节点

删除链表尾部节点的处理流程如下:

  • 遍历链表找到链表的倒数第二个节点,记作preNode
  • 通过preNode找到要删除的节点,记作delNode
    • delNode = preNode->next
  • 让preNode节点的指针域指向空地址,有两种处理方式
    • preNode->next = nullptr
    • preNode->next = delNode->next
  • 释放delNode节点指向的内存资源

        通过以上两种场景下操作流程的对比可以得到一个结论:对于带头结点的单向链表,删除链表中任意位置的节点的处理流程都是相同的。

3. 单向链表的实现

3.1 带头结点的单向链表

        在进行链表操作的时候,为了能够提高操作效率以及使用起来更加方便,除了给链表添加一个头指针以外,还可以提供一个尾指针,有了尾指针之后访问链表尾节点的时候时间复杂度就从O(n)变成了O(1)。同理,给链表添加了长度成员之后,每次想要得到链表的长度,就不需要进行遍历了。

头文件

// SLinkList.h
#pragma oncestruct Node
{int data = 0;Node* next = nullptr;
};// 定义单向链表类
class LinkList
{
public:LinkList();~LinkList();// 判断链表是否为空bool isEmpty();// 获取链表节点数量int length();// 数据添加到链表头部void prepend(int data);// 数据添加到链表尾部void append(int data);// 数据插入到链表任意位置, 第一个数据元素 pos=1bool insert(int pos, int data);// 搜索数值, 返回节点和位置, 没找到返回nullptrNode* find(int data, int& pos);// 删除节点bool remove(int pos);// 遍历链表void display();// 返回头结点inline Node* head() { return m_head; }// 返回指定位置的节点的值int value(int pos);private:int m_length = 0;Node* m_head = nullptr;Node* m_tail = nullptr;
};

关于上面链表类中的各个操作函数都有对应的注释说明,在此就不再赘述了。

源文件

// SLinkList.cpp
#include "SLinkList.h"
#include <iostream>
#include <cassert>
using namespace std;LinkList::LinkList() : m_head(new Node)
{if (m_head == nullptr) {return;}m_head->next = nullptr;m_tail = m_head;
}LinkList::~LinkList()
{Node* p = m_head;while (p != nullptr){Node* tmp = p;cout << "释放资源: " << tmp->data << endl;p = p->next;delete tmp;}
}bool LinkList::isEmpty()
{bool flag = m_head->next == nullptr;return flag;
}int LinkList::length()
{return m_length;
}void LinkList::prepend(int data)
{// 创建新节点Node* pNode = new Node;if (pNode == nullptr){cout << "头部添加链表节点失败!\n" << endl;return;}// 初始化pNode->data = data;pNode->next = m_head->next; if (m_head->next == nullptr) {m_tail = pNode;}m_head->next = pNode;m_length++;
}void LinkList::append(int data)
{Node* pNode = new Node;pNode->data = data;m_tail->next = pNode;m_tail = pNode;m_length++;
}bool LinkList::insert(int pos, int data)
{if (pos < 1 || pos > length()+1){return false;}Node* p = m_head;int j = 0;while (p != nullptr && j < pos - 1){p = p->next;j++;}Node* pNode = new Node;pNode->data = data;if (length()+1 == pos){m_tail = pNode;}pNode->next = p->next;p->next = pNode;m_length++;return true;
}Node* LinkList::find(int data, int& pos)
{pos = 1;Node* p = m_head->next;while (p != nullptr && p->data != data){p = p->next;pos++;}return p;
}bool LinkList::remove(int pos)
{if (pos < 1 || pos > length()){cout << "删除失败, 无效的节点位置" << endl;return false;}Node* p = m_head;for (int i = 0; i < pos - 1; ++i){p = p->next;}Node* delNode = p->next;p->next = delNode->next;if (delNode->next == nullptr){m_tail = p;}delete delNode;m_length--;return true;
}void LinkList::display()
{Node* p = m_head->next;if (p == nullptr){cout << "空链表" << endl;return;}cout << "链表值: ";while (p != nullptr){cout << p->data << " ";p = p->next;}cout << endl;
}int LinkList::value(int pos)
{assert(pos > 0 && pos <= length());Node* p = m_head;for (int i = 0; i < pos; ++i){p = p->next;}return p->data;
}int main()
{LinkList lst;bool flag = lst.isEmpty();cout << "链表是否为空: " << flag << endl;lst.insert(1, 88);lst.prepend(10);lst.append(20);lst.prepend(30);lst.insert(2, 40);lst.insert(1, 50);lst.insert(6, 60);lst.append(100);cout << "链表长度尾: " << lst.length() << endl;lst.display();int pos = 0;Node* node = lst.find(50, pos);cout << "元素的位置: " << pos << ", 元素值: " << node->data << endl;lst.remove(pos);lst.display();node = lst.find(100, pos);cout << "元素的位置: " << pos << ", 元素值: " << node->data << endl;lst.remove(pos);lst.display();node = lst.find(10, pos);cout << "元素的位置: " << pos << ", 元素值: " << node->data << endl;lst.remove(pos);lst.display();lst.append(200);lst.display();return 0;
}

对于上面链表类中的某些API函数带有一个整形的节点位置pos,该位置的值是从1开始的,也就是说链表中第一个数据节点的位置是1。

原文链接: https://subingwen.cn/data-structure/singly-linked-list/#%E6%BA%90%E6%96%87%E4%BB%B6
 

这篇关于线性表之单向链表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1127848

相关文章

csu1329(双向链表)

题意:给n个盒子,编号为1到n,四个操作:1、将x盒子移到y的左边;2、将x盒子移到y的右边;3、交换x和y盒子的位置;4、将所有的盒子反过来放。 思路分析:用双向链表解决。每个操作的时间复杂度为O(1),用数组来模拟链表,下面的代码是参考刘老师的标程写的。 代码如下: #include<iostream>#include<algorithm>#include<stdio.h>#

深入手撕链表

链表 分类概念单链表增尾插头插插入 删尾删头删删除 查完整实现带头不带头 双向链表初始化增尾插头插插入 删查完整代码 数组 分类 #mermaid-svg-qKD178fTiiaYeKjl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-

建立升序链表

题目1181:遍历链表 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:2744 解决:1186 题目描述: 建立一个升序链表并遍历输出。 输入: 输入的每个案例中第一行包括1个整数:n(1<=n<=1000),接下来的一行包括n个整数。 输出: 可能有多组测试数据,对于每组数据, 将n个整数建立升序链表,之后遍历链表并输出。 样例输

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟)

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟) 题目描述 给定一个链表,链表中的每个节点代表一个整数。链表中的整数由 0 分隔开,表示不同的区间。链表的开始和结束节点的值都为 0。任务是将每两个相邻的 0 之间的所有节点合并成一个节点,新节点的值为原区间内所有节点值的和。合并后,需要移除所有的 0,并返回修改后的链表头节点。 思路分析 初始化:创建一个虚拟头节点

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图

数据结构:线性表的顺序存储

文章目录 🍊自我介绍🍊线性表的顺序存储介绍概述例子 🍊顺序表的存储类型设计设计思路类型设计 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞+关注+评论+收藏(一键四连)哦~ 🍊自我介绍   Hello,大家好,我是小珑也要变强(也是小珑),我是易编程·终身成长社群的一名“创始团队·嘉宾” 和“内容共创官” ,现在我来为大家介绍一下有关物联网-嵌入

[数据结构]线性表之单链表的类模板实现

类的具体实现如下: /#include"LinearList.h"#include <iostream>#include <cstdlib>using namespace std;template<class T>struct LinkNode //链表节点类{T data;LinkNode<T>* link;LinkNode(LinkNode<T>* ptr=NULL):

【数据结构与算法 | 灵神题单 | 删除链表篇】力扣3217, 82, 237

总结,删除链表节点问题使用到列表,哈希表,递归比较容易超时,我觉得使用计数排序比较稳,处理起来也不是很难。 1. 力扣3217:从链表中移除在数组中的节点 1.1 题目: 给你一个整数数组 nums 和一个链表的头节点 head。从链表中移除所有存在于 nums 中的节点后,返回修改后的链表的头节点。 示例 1: 输入: nums = [1,2,3], head = [1,2,3,

c++ 链表详细介绍

链表是数据结构的一种,由节点组成,每个节点包含数据和指向下一个节点的指针。链表在C++中的实现可以是单链表、双链表或循环链表。以下是链表的详细介绍: 1. 单链表 结构: 节点(Node):每个节点包含数据和一个指针(next),指向链表中的下一个节点。 示例结构: struct Node {int data;Node* next;Node(int d) : data(d), next(

带头结点的线性链表的基本操作

持续了好久,终于有了这篇博客,链表的操作需要借助图像模型进行反复学习,这里尽可能的整理并记录下自己的思考,以备后面复习,和大家分享。需要说明的是,我们从实际应用角度出发重新定义了线性表。 一. 定义 从上一篇文章可以看到,由于链表在空间的合理利用上和插入、删除时不需要移动等优点,因此在很多场合下,它是线性表的首选存储结构。然而,它也存在某些实现的缺点,如求线性表的长度时不如顺序存储结构的