数据结构与算法【02】—线性表

news2024/11/28 20:41:33

CSDN系列专栏:数据结构与算法专栏
针对以前写的数据结构与算法系列重写(针对文字描述、图片、错误修复),改动会比较大,一直到更新完为止

前言

通过前面数据结构与算法基础知识我们知道了数据结构的一些概念和重要性,那么本章总结下线性表相关的内容。当然,我用自己的理解分享给大家。

其实说实话,可能很多人依然分不清线性表顺序表,和链表之间的区别和联系!

  • 线性表:逻辑结构, 就是对外暴露数据之间的关系,不关心底层如何实现,数据结构的逻辑结构大分类就是线性结构和非线性结构而顺序表、链表都是一种线性表。
  • 顺序表、链表:物理结构,他是实现一个结构实际物理内存上的结构。比如顺序表就是用数组实现。而链表主要用指针完成工作。不同的结构在不同的场景有不同的区别。

在Java中,大家都知道List接口,这就是逻辑结构,它封装了一个线性关系的一系列方法,用于表示和维护线性关系。而具体的实现其实就是跟物理结构相关的内容。比如顺序表的内容存储使用数组的,然后一个get,set,add方法都要基于数组来完成,而链表是基于指针的,基于不同的物理结构要根据结构的特性维护数据的存储和线性关系。

下面用一个图来浅析物理结构中顺利表和链表之间的区别。

image-20210104160901005

线性表基本架构

对于一个线性表来说,不管它的具体实现如何,它们的方法函数名和实现效果应该一致(即使用方法相同、达成逻辑上的效果相同,差别的是实现方式可能针对不同的场景效率不同)。线性表的概念与Java的接口/抽象类有一些相似之处。最著名的例子就是List接口的ArrayList和LinkedList,List是一种逻辑上的结构,表示这种结构为线性表,而ArrayList和LinkedList更多的是一种物理结构(数组和链表)。

所以基于面向对象的编程思维,我们可以将线性表写成一个接口,而具体实现的顺序表和链表的类可以实现这个线性表的方法,以提高程序的可读性。还有一点非常重要,初学数据结构与算法时实现的线性表都是固定类型(例如int),随着知识的进步,我们应当采用泛型来实现更合理的方式。至于接口的具体设计如下:

public interface ListInterface<T> {
    void init(int initialSize); // 初始化表
    int length();
    boolean isEmpty(); // 是否为空
    int elemIndex(T t); // 找到编号
    T getElem(int index); // 根据index获取数据
    void add(int index, T t) ; // 根据index插入数据
    void delete(int index) ;
    void add(T t) ; // 尾部插入
    void set(int index, T t) ;
    String toString(); // 转成String输出
}

顺序表

顺序表是基于数组实现的,所有实现需要基于数组特性。对于顺序表的结构应该有一个存储数据的数组data和有效使用长度size.

这里为了简单就不实现扩容、异常处理相关的操作。

下面着重讲解一些初学者容易混淆的概念和方法实现。这里把顺序表比作一队坐在板凳上的人。

插入操作

add(int index,T value)

其中index为插入的编号位置,value为插入的数据,插入的流程为:

(1)从后(最后一个有数据位)向前到index依次后移一位,腾出index位置的空间

(2)将待插入数据赋值到index位置上,完成插入操作

image-20231029221307141

顺序表很长,在靠前的地方如果插入效率比较低(插入时间复杂度为O(n)),如果频繁的插入那么复杂度挺高的。

删除操作

同理,删除原理和插入类似,删除index位置的操作就是从index开始(index+1)数据赋值到index位置,一直到size-1位置,具体可以看这张图:

image-20231029221644046

代码实现

这里实现一个简单的顺序表:

public class SeqList<T> implements ListInterface<T> {
    private T[] array;
    private int size;

    public SeqList() {
        // 默认构造函数
         init(10);
    }

    @Override
    public void init(int initialSize) {
        array = (T[]) new Object[initialSize];
        size = 0;
    }

    @Override
    public int length() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int elemIndex(T value) {
        for (int i = 0; i < size; i++) {
            if (array[i].equals(value)) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public T getElem(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds.");
        }
        return array[index];
    }

    @Override
    public void add(int index, T value) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Index is out of bounds.");
        }
        if (size == array.length) {
            // 如果数组已满,扩展数组
            resizeArray();
        }
        // 将index之后的元素后移一位
        for (int i = size; i > index; i--) {
            array[i] = array[i - 1];
        }
        array[index] = value;
        size++;
    }

    @Override
    public void delete(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds.");
        }
        // 将index之后的元素前移一位
        for (int i = index; i < size - 1; i++) {
            array[i] = array[i + 1];
        }
        size--;
    }

    @Override
    public void add(T value) {
        if (size == array.length) {
            // 如果数组已满,扩展数组
            resizeArray();
        }
        array[size] = value;
        size++;
    }

    @Override
    public void set(int index, T value) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds.");
        }
        array[index] = value;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < size; i++) {
            sb.append(array[i]);
            if (i < size - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }

    private void resizeArray() {
        int newCapacity = (int) (array.length * 1.5);
        T[] newArray = (T[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newArray[i] = array[i];
        }
        array = newArray;
    }
}

链表

在学习C/C++时,链表往往让许多人感到复杂,其中一个主要原因可能是因为涉及到指针。尽管在Java中不直接使用指针,但我们仍然需要理解指针的原理和应用。链表与顺序表(数组)不同,它的结构就像一条链一样,将节点链接成一个线性结构。在链表中,每个节点都存在于不同的内存地址中,指针指向(链表存储)了相邻节点的地址,节点能够通过这些指针找到下一个的节点形成一条链。

就物理存储结构而言,地址之间的联系是无法更改的,相邻地址就是相邻。但在链式存储中,下一个地址是由上一个节点"主动记录的",因此可以进行更改。这就好比亲兄弟从出生就是同姓兄弟,而在我们的成长过程中,最好的朋友可能会因为阶段性的变化而有所不同!

举个例子,就像西天取经的唐僧、悟空、八戒、沙和尚。他们本来没有直接的联系,但通过结拜为师徒兄弟,他们建立了联系。如果你问悟空他的师父是谁,他会立刻想到唐僧,因为他们之间有五指山下的约定。

image-20231029223556929

基本结构

对于线性表,我们只需要一个data数组和size就能表示基本信息。而对于链表,我们需要一个Node类节点(head头节点),和size分别表示存储的节点数据和链表长度,这个节点有数据域指针域。数据域就是存放真实的数据,而指针域就是存放下一个Node类节点的指针,其具体结构为:

 private static class Node<T> {
   T data;
   Node<T> next;

   Node(T data) {
     this.data = data;
     this.next = null;
   }
 }

带头结点链表VS不带头结点链表

有许多人可能会对带头结点和不带头结点链表的区别感到困惑,甚至不清楚什么是带头结点和不带头结点。我来为大家阐述一下:

带头结点:在带头结点的链表中,head指针始终指向一个节点,这个节点不存储有效值,仅仅起到一个标识作用(有点像班主任带着学生)。

不带头结点:在不带头结点的链表中,head指针始终指向第一个有效节点,这个节点存储有效数值。

那么带头结点和不带头结点的链表有什么区别呢?

查找方面:在查找操作上,它们没有太大区别,带头结点需要多进行一次查找。

插入方面:对于非第0个位置的插入操作,区别不大,但不带头结点的链表在插入第0号位置之后需要重新改变head头指针的指向。

image-20231029224835482

删除方面:对于非第0个位置的删除操作,区别不大,不带头结点的链表在删除第0号位置之后需要重新改变head头指针的指向。

  • 头部删除(带头结点):在带头结点的链表中,头部删除操作和普通删除操作一样。只需执行 head.next = head.next.next,这样head的next直接指向第二个元素,从而删除了第一个元素。
  • 头部删除(不带头结点):不带头结点的链表的第一个节点(head)存储有效数据。在不带头结点的链表中,删除也很简单,只需将head指向链表中的第二个节点即可,即:head = head.next

image-20231029225238384

总而言之:带头结点通过一个固定的头可以使链表中任意一个节点都同等的插入、删除。而不带头结点的链表在插入、删除第0号位置时候需要特殊处理,最后还要改变head指向。两者区别就是插入删除首位(尤其插入),个人建议以后在使用链表时候尽量用带头结点的链表避免不必要的麻烦。

带头指针VS带尾指针

基本上是个链表都是要有头指针的,那么头尾指针是个啥呢?

头指针: 其实头指针就是链表中head节点,表示链表的头,称为为头指针。

**尾指针: **尾指针就是多一个tail节点的链表,尾指针的好处就是进行尾插入的时候可以直接插在尾指针的后面。

image-20231029225930693

但是带尾指针的单链表如果删除尾的话效率不高,需要枚举整个链表找到tail前面的那个节点进行删除。

插入操作

add(int index,T value)
其中index为插入的编号位置,value为插入的数据,在带头结点的链表中插入那么操作流程为

  1. 找到对应index-1号节点成为pre。
  2. node.next=pre.next,将插入节点后面先与链表对应部分联系起来。此时node.next和pre.next一致。
  3. pre.next=node 将node节点插入到链表中。

image-20231029231042857

当然,很多时候链表需要插入在尾部,如果频繁的插入在尾部每次枚举到尾部的话效率可能比较低,可能会借助一个尾指针去实现尾部插入。

删除操作

按照index移除(主要掌握):delete(int index)

带头结点链表的通用方法(删除尾也一样),找到该index的前一个节点pre,pre.next=pre.next.next

image-20231029231902762

代码实现

在这里我也实现一个单链表给大家作为参考使用:

public class LinkedList<T> implements ListInterface<T> {
    private Node<T> head;
    private int size;

    public LinkedList() {
        head = new Node<>(null); // 头结点不存储数据
        size = 0;
    }

    @Override
    public void init(int initialSize) {
        head.next = null;
        size = 0;
    }

    @Override
    public int length() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int elemIndex(T value) {
        Node<T> current = head.next;
        int index = 0;
        while (current != null) {
            if (current.data.equals(value)) {
                return index;
            }
            current = current.next;
            index++;
        }
        return -1;
    }

    @Override
    public T getElem(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds.");
        }
        Node<T> current = head.next;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        return current.data;
    }

    @Override
    public void add(int index, T value) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Index is out of bounds.");
        }
        Node<T> newNode = new Node<>(value);
        Node<T> pre = head;
        for (int i = 0; i < index; i++) {
            pre = pre.next;
        }
        newNode.next = pre.next;
        pre.next = newNode;
        size++;
    }

    @Override
    public void delete(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds.");
        }
        Node<T> pre = head;
        for (int i = 0; i < index; i++) {
            pre = pre.next;
        }
        pre.next = pre.next.next;
        size--;
    }

    @Override
    public void add(T value) {
        add(size, value); // 在末尾添加元素
    }

    @Override
    public void set(int index, T value) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index is out of bounds.");
        }
        Node<T> current = head.next;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        current.data = value;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        Node<T> current = head.next;
        while (current != null) {
            sb.append(current.data);
            if (current.next != null) {
                sb.append(", ");
            }
            current = current.next;
        }
        sb.append("]");
        return sb.toString();
    }

    private static class Node<T> {
        T data;
        Node<T> next;

        Node(T data) {
            this.data = data;
            this.next = null;
        }
    }

    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        list.init(10); // 初始化表
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(1, 4); // 在索引1处插入值4
        list.delete(2); // 删除索引2处的值
        System.out.println(list.toString()); // 打印表的内容
    }
}

总结

这里的只是简单实现,实现基本方法。链表也只是单链表。完善程度还可以优化。

单链表查询速度较慢,因为他需要从头遍历,如果在尾部插入,可以考虑设计带尾指针的链表。而顺序表查询速度虽然快但是插入很费时,实际应用根据需求选择

Java中的Arraylist和LinkedList就是两种方式的代表,不过LinkedList使用双向链表优化,并且JDK也做了大量优化。所以大家不用造轮子,可以直接用,但是手写顺序表、单链表还是很有学习价值的。

CSDN专栏:数据结构与算法专栏
开源仓库:bigsai-algorithm仓库 ,欢迎支持
如果觉得不错 还请三连支持一下!

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

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

相关文章

新技术前沿-2023-应用GPT提问模板写技术文章

参考一份万能的GPT提问模版&#xff01;直接套用&#xff01; 参考用GPT写技术文章是真爽&#xff01; 参考码住这篇 8200 字 ChatGPT 实战指南&#xff01;&#xff01; 1 GPT提问模板 想让GPT回答的内容符合我们所希望的&#xff0c;最最重要的一点就在于我们如何提问。提问…

NFS服务以及静态路由及临时IP配置

目录 一、NFC服务基础知识 1、NFS服务初相识 2、NFS服务工作原理 二、NFC服务基础操作 1、NFS服务端配置 2、NFS服务 - exports 相关参数 3、NFS服务 - 命令相关 三、RPC 远程调度 四、静态路由及临时IP配置 1、Linux 静态路由相关命令 2、Linux 临时IP地址添加与删除…

【漏洞复现】Nginx_0.7.65_空字节漏洞

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞扫描3、漏洞验证 1.1、漏洞描述 1.2、漏洞等级 1.3、影响版本 0.7.65 1.4、漏洞复现 1、基础环…

Redis那些事儿(三)

文章目录 1. 前言2. 常用api介绍3. 需求假设&#xff08;获取离我最近的停车场&#xff09;4. 代码示例 1. 前言 接着上一篇Redis那些事儿&#xff08;二&#xff09; &#xff0c;这一篇主要介绍Redis基于Geo数据结构实现的地理服务&#xff0c;它提供了一种方便的方式来存储和…

linux 创建git项目并提交到gitee(保姆式教程)

01、git安装与初始化设置 mhzzjmhzzj-virtual-machine:~/work/skynetStudy$ apt install mhzzjmhzzj-virtual-machine:~/work/skynetStudy$ git config --global user.name "用户名" mhzzjmhzzj-virtual-machine:~/work/skynetStudy$ git config --global user.ema…

Instant-NGP论文笔记

文章目录 论文笔记 论文笔记 instant-ngp的nerf模型与vanilla nerf的模型架构相同。 instant-ngp的nerf模型包含两个MLP&#xff0c;第一个MLP就两个全连接&#xff0c;输入维度是32&#xff08;16层分辨率x2&#xff09;&#xff0c;输出是16&#xff08;用于预测密度&#x…

SpringBoot配置文件优先级

1.idea临时属性 说明&#xff1a;Program arguments配置--server.port8082 --ab&#xff1b;意思是将端口改成了8082。这个优先级最高。 2.resource 说明&#xff1a;创建config文件里面的yml文件。 3.jar包同级&#xff08;yml&#xff09; 说明&#xff1a;创建一个yml文件…

机器学习中的关键组件

机器学习中的关键组件 数据 每个数据集由一个个样本组成&#xff0c;大多时候&#xff0c;它们遵循独立同分布。样本有时也叫作数据点或数据实例&#xff0c;通常每个样本由一组称为特征或协变量的属性组成。机器学习会根据这些属性进行预测&#xff0c;预测得到的称为标签或…

平面扫描(Plane-sweeping)深度体会

先看文章 三维重建之平面扫描算法&#xff08;Plane-sweeping&#xff09;_plane sweeping_小玄玄的博客-CSDN博客 Plane Sweeping | 平面扫描 - 知乎 (zhihu.com) 注意平面Dm,这是其中一个平面&#xff0c;平面上有一个M点&#xff0c;这个点也再物体上。所以会被摄像机看到…

Idea去掉显示的测试覆盖率

一.启东时 误点击了 快捷键调出 【Ctrl 】【Alt】【F6】

优雅的 Dockerfile 是怎样炼成的?

Docker 简介 目前&#xff0c;Docker 主要有两个形态&#xff1a;Docker Desktop 和 Docker Engine。 Docker Desktop 是专门针对个人使用而设计的&#xff0c;支持 Mac&#xff08;已支持arm架构的M系芯片&#xff09; 和 Windows 快速安装&#xff0c;具有直观的图形界面&a…

数据结构—字符串

文章目录 7.字符串(1).字符串及其ADT#1.基本概念#2.ADT (2).字符串的基本操作#1.求子串substr#2.插入字符串insert#3.其他操作 (3).字符串的模式匹配#1.简单匹配(Brute-Force方法)#2.KMP算法I.kmp_match()II.getNext() #3.还有更多 小结附录&#xff1a;我自己写的string 7.字符…

手搭手Ajax实现搜索地址自动补全功能

输入单词后&#xff0c;自动提示出要搜索的信息&#xff0c;点击某个内容后&#xff0c;自动补全至搜索框。 比如&#xff1a; 如何实现搜索自动补全功能 键盘事件&#xff1a;keyup按键弹起事件发送ajax请求&#xff0c;请求中提交用户输入的搜索内容,后端接收内容后&#x…

23种设计模式-Java语言实现

因为要准备一个考试所以又重新接触到了设计模式&#xff0c;之前只是别人说什么就是什么&#xff0c;记下就好了&#xff0c;完全不理解其中的思想以及为什么要用(虽然现在也不太理解…) 先慢慢总结吧&#xff0c;常读常新。 23种设计模式 “每一个模式描述了一个在我们周围不…

C++进阶篇4---set和map

一、关联式容器 在初阶篇中&#xff0c;我们已经接触过STL中的部分容器&#xff0c;比如&#xff1a;vector、list、deque等&#xff0c;这些容器统称为序列式容器&#xff0c;因为其底层为线性序列的数据结构&#xff0c;里面存储的是元素本身。 那什么是关联式容器&#xff1…

【unity实战】Unity实现2D人物双击疾跑

最终效果 前言 我们要实现的功能是双击疾跑&#xff0c;当玩家快速地按下同一个移动键两次时能进入跑步状态 我假设快速按下的定义为0.2秒内&#xff0c;按下同一按键两次 简单的分析一下需求&#xff0c;实现它的关键在于获得按键按下的时间&#xff0c;我们需要知道第一次…

eBPF BCC开源工具简介

目录 官方链接 编译安装 ubuntu版本 安装 examples tools hello_world.py demo 运行报错 网上目前的解决办法 错误分析过程 python版本检测 libbcc库检查 python3 bcc库检查 正常输出 监控进程切换 运行输出 监控CPU直方图 缓存命中率监控&#xff1a;caches…

英语——分享篇——每日200词——201-400

201——feel——[fi:l]——vt.摸&#xff0c;感觉&#xff0c;认为&#xff1b;n.感觉&#xff0c;触摸——feel——f斧头(编码)ee眼睛(象形)l棍子(编码)——斧头用眼看&#xff0c;棍子用手摸——The metal felt smooth and cold.——这种金属摸起来冰冷而光滑。 202——cleve…

SpringBoot项目打包与运行

1.clean生命周期 说明&#xff1a;为了项目能够正确打包&#xff0c;先清理打包文件。 2.package生命周期 说明&#xff1a;打包后生成以下目录。 2.1问题 说明&#xff1a;springboot_08_ssmp-0.0.1-SNAPSHOT.jar中没有主清单属性。 2.2解决 说明&#xff1a;注释skip&…

[LeetCode]-160. 相交链表-141. 环形链表-142.环形链表II-138.随机链表的复制

目录 160.相交链表 题目 思路 代码 141.环形链表 题目 思路 代码 142.环形链表II 题目 思路 代码 160.相交链表 160. 相交链表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/intersection-of-two-linked-lists/description/ 题目 给你两个…