浅谈软件编程中的8大数据结构

简介: 浅谈软件编程中的8大数据结构

前言


其实在Java编程中大家一直都比较忽视数据结构,大部分都是常用几个集合类型,能实现业务功能就行,对数据结构都缺乏系统认知。最近计划翻翻源码,对java中常用的数据结构加深下理解。


一、为什么要研究数据结构


应付大厂面试,升职加薪

熟悉数据结构可以帮忙我们更好的理解源码和架构设计,明白设计的精妙之处

数据结构可以帮助我们进行程序的性能优化

数据结构是软件编程的基石,具有普遍的适用性,俗称的“内功”,不像具体的技术栈,很容易过时。


二、数据结构的分类


程序 = 算法 + 数据结构


数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。可分为八大数据结构:数组(Array)、栈(Stack)、链表(Linked List)、图(Graph)、散列表(Hash)、队列(Queue)、树(Tree)、堆(Heap)。

114.png


1.数组(Array)

数组是最基本的数据结构,很多语言都内置支持数组。数组是使用一块连续的内存空间保存数据,存储一组具有相同类型的数据,保存的数据个数在分配内存的时候就是确定的。

113.png


访问数组中第n个数据(即根据数据的下标随机访问)的时间复杂度是O(1);但是要在数组中查找一个指定的数据则是O(N);

当向数组中插入或者删除数据的时候:

最好的情况是在数组的末尾进行操作,时间复杂度是O(1) ;

最坏情况是插入或者删除第一个数据,时间复杂度是 O(N) 。在数组的任意位置插入或者删除数据的时候,后面的数据全部需要移动,移动的数据还是和数据个数有关所以总体的时间复杂度仍然是 O(N) 。平均情况时间复杂度也为O(n)。

112.png

特点:按照下标(索引)查询速度快、遍历数组方便。


限制:


数组大小固定后无法扩容;


数组只能存储一种类型的数据;


添加删除慢(需要移动其它元素);


使用场景:

频繁查询,对存储空间要求不大、增加删除少的场景。


2.链表(Linked List)

链表是在非连续的内存单元中保存数据,并且通过指针将各个内存单元链接在一起,最右一个节点的指针指向NULL。链表不需要提前分配固定大小存储空间,当需要存储数据的时候分配一块内存并将这块内存插入链表中。


链表是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个节点,一个是存储元素的数据域(内存空间),另一个是指向下一个节点的指针域。根据指针的指向,链表能形成不同的结构。


在链表中查找第n个数据以及查找指定的数据的时间复杂度是O(N),但是插入和删除数据的时间复杂度是O(1),因为只需要调整指针就可以。

111.png

像上面这样的链表结构在插入和删除的时候编程会比较困难,因为需要记住当前节点的前一个节点,这样才能完成插入和删除。为了简便通常使用带有头节点的链表:

110.png

单链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。

109.png

头指针就是链表的名字,头指针仅仅是个指针而已。


添加头结点的好处:


主要是为了统一操作。头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。

有了头结点后,对在第一个元素结点前插入结点和删除第一个结点,其操作与对其它结点的操作统一了。

上面的链表是单链表,此外还有双链表,就是节点中包含指向下一个节点的指针和指向上一个节点的指针:

108.png

不带有头节点的双向链表在插入和删除数据的时候也不会出现单链表那样的问题。此外还有一种链表是循环链表,它是将双向链表的头尾相接:

107.png

向循环双向链表和循环链表中插入或者从中删除数据只是多移动几个指针。

特点:

很常用的一种数据结构,不需要初始化容量,可以任意加减元素;

添加或删除元素时只需要改变前后两个元素节点的指针域指向地址即可,所以添加删除速度很快。


限制:

因含有大量的指针域,占用空间较大;

查找元素需要遍历链表来查找,非常耗时。


使用场景:

数据量较小,需要频繁添加删除操作的场景。


注意⚠️:

有一种说法是软件编程中只有两种数据结构:数组和链表。其他数据结构都是基于这两种数据结构的演化,由此可见数组和链表这两种数据结构的重要性。


3.队列(Queue)

队列实现了先入先出的语义,队列也可以使用数组和链表来实现。队列也是一种线性表 。不同的是,队列可以在一端添加元素,在另一端取出元素 。

106.png

队列只允许在队尾添加数据,在队头删除数据。但是可以查看队头和队尾的数据。还有一种是双端队列,在两端都可以插入和删除:

105.png

特点:先进先出(FIFO);

使用场景:因为队列先进先出的特点,在多线程阻塞队列管理中非常适用,MQ消息顺序消费,集合对象的有序遍历。


队列(Queue)在java体系中既可以采用数组实现,又可以采用双向链表实现。


ArrayDeque基于数组实现的双向链表,可以满足队列先进先出(FIFO)的特性。

104.png


LinkedList基于链表实现双向链表,也可以满足队列先进先出(FIFO)的特性。

103.png


4.栈(Stack)

栈或者称为堆栈,栈是一种特殊的线性表(桶状线性数据结构)。仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作 。

102.png

特点:先进后出(FILO);


限制:只允许操作栈顶,不允许操作栈底;


使用场景:栈常应用于实现递归功能方面的场景,例如斐波那契数列。


java中栈(Stack)的相关实现:

Deque双端队列也可以用作 LIFO(后进先出)堆栈。官方推荐应优先使用Deque接口而不是旧的Stack类。当双端队列用作堆栈时,从双端队列的开头推送和弹出元素。 Stack 方法完全等同于Deque方法,如下表所示:

Stack 和 Deque 方法的比较

堆栈方法 等效Deque方法
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()
请注意,当双端队列用作队列或堆栈时, peek方法同样有效;



注意⚠️:

Java中的Deque双端队列也有2种实现,一种是基于数组,一种是基于链表。也再次验证了数组和链表在数据结构中的核心地位。


基于数组:

Deque<Integer> stack = new ArrayDeque<Integer>();


基于链表

Deque<TreeNode> stack = new LinkedList<Integer>();


有刷过leetcode的同学都会发现,现在关于递归和栈的算法题中,栈都是采用的Deque了:


比如:利用栈替代递归实现树的前序遍历

public List<Integer> preorderTraversal(TreeNode root) {
        //采用非递归方式实现前序遍历
        List<Integer> res = new ArrayList<Integer>();
        if(root ==  null){
            return res;
        }
        //java官方文档推荐用deque实现栈(stack)。
        //push压入,pop弹出,peek取出但不弹出
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        TreeNode node = root;
        while (!stack.isEmpty() || node != null) {
            while(node != null){
                //前序遍历,先访问根节点
                res.add(node.val);
                stack.push(node);
                //继续遍历下一个左节点
                node = node.left;
            }
            //FIFO倒序遍历每个节点的右节点
            node = stack.pop();
            node = node.right;
        }
        return  res;
    }


5.散列表(Hash)

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。针对某个数据的等值查询时间复杂度为O(1)。


数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。


那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。


优点:一对一的查找效率很高;

缺点:一个关键字可能对应多个散列地址;需要查找一个范围时,效果不好。

hash冲突:不同的关键字经过散列函数的计算得到了相同的散列地址。


好的散列函数=计算简单+分布均匀(计算得到的散列地址分布均匀)


哈希表是种数据结构,它可以提供快速的插入操作和快速精确查找操作。


在java体系中,HashMap就是典型的散列表(Hash)结构。

其底层实现就是:数组 + 链表+(红黑树)

101.png


6.树(Tree)

维基百科中树的定义:

在计算机科学中,树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:


每个节点都只有有限个子节点或无子节点;

没有父节点的节点称为根节点;

每一个非根节点有且只有一个父节点;

除了根节点外,每个子节点可以分为多个不相交的子树;

树里面没有环路(cycle)

树又分为很多种类,因使用场景的不同被定义出很多类型:


无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;

有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;

二叉树:每个节点最多含有两个子树的树称为二叉树;

完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个节点的二叉树,当且仅当其每一个节点都与深度为K的满二叉树中编号从1至n的节点一一对应时称之为完全二叉树。

100.png

public class TreeNode {
     int val;
     TreeNode left;
     TreeNode right;
     TreeNode() {}
     TreeNode(int val) { this.val = val; }
     TreeNode(int val, TreeNode left, TreeNode right) {
         this.val = val;
         this.left = left;
         this.right = right;
     }
 }


定义树的一个节点:


注意⚠️:

树可以理解成一种特殊的链表结构。


7.堆(Heap)

**堆(Heap)**是计算机科学中一类特殊的数据结构的统称,堆通常是一个可以被看做一棵完全二叉树的数组对象,根节点可以为大于等于任何子节点(也可以小于等于任意子节点,看具体的排序方法),在存取时没有任何限制,可以随意的访问某一个子节点。最大堆:每个节点的值都大于等于它的孩子节点。最小堆:每个节点的值都小于等于它的孩子节点。


注意⚠️:

特别要声明和注意的是,这里的堆仅仅是存储数据的一种结构方式,与内存的堆栈不是一个概念。


特点:


堆中某个节点的值总是不大于或不小于其父节点的值;

堆总是一棵完全二叉树。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

98.png

99.png


堆的作用:


构建优先队列

支持堆排序

快速找出一个集合中的最小值(或者最大值)

在朋友面前装逼

堆和普通树的区别

堆并不能取代二叉搜索树,它们之间有相似之处也有一些不同。我们来看一下两者的主要差别:


节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。


内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配内存。堆仅仅使用一个数据来存储数组,且不使用指针。


平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足堆属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(log n) 的性能。


搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入、删除操作。


优先队列:出队与入队时的顺序无关,与优先级有关。

堆是优先队列这种数据结构的一种实现方式。

/**
 * 基于数组实现的最大堆
 * 堆中的元素需要具有可比较性,所以需要实现Comparable
 * 在此实现中是从数组的下标0开始存储元素,因为使用ArrayList作为数组的角色
 *
 * @author 01
 * @date 2021-01-19
 **/
public class MaxHeap<E extends Comparable<E>> {
    /**
     * 使用ArrayList的目的是无需关注动态扩缩容逻辑
     */
    private final ArrayList<E> data;
    public MaxHeap(int capacity) {
        this.data = new ArrayList<>(capacity);
    }
    public MaxHeap() {
        this.data = new ArrayList<>();
    }
    /**
     * 返回对中的元素个数
     */
    public int size() {
        return data.size();
    }
    /**
     * 判断堆是否为空
     */
    public boolean isEmpty() {
        return data.isEmpty();
    }
    /**
     * 根据传入的index,计算其父节点所在的下标
     */
    private int parent(int index) {
        if (index == 0) {
            throw new IllegalArgumentException("index-1 doesn't have parent.");
        }
        return (index - 1) / 2;
    }
    /**
     * 根据传入的index,计算其左子节点所在的下标
     */
    private int leftChild(int index) {
        return index * 2 + 1;
    }
    /**
     * 根据传入的index,计算其右子节点所在的下标
     */
    private int rightChild(int index) {
        return index * 2 + 2;
    }
}


8.图(Graph)

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。在图中的数据元素,我们称之为顶点(Vertex),顶点集合有穷非空。在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。


图的数据结构还可能包含和每条边相关联的数值(edge value),例如一个标号或一个数值(即权重,weight;表示花费、容量、长度等)。

97.png

java中遇到的场景真的不多,状态机、图数据库等。


总结


本文主要对软件编程中常见的8种数据结构进行了概要说明。

1、八大数据结构:数组(Array)、栈(Stack)、链表(Linked List)、图(Graph)、散列表(Hash)、队列(Queue)、树(Tree)、堆(Heap)。

2、其中数组(Array)和链表(Linked List)是核心,其他很多结构都是基于这两种结构演变而来。


MySQL之InnoDB存储结构总结


相关实践学习
简单用户画像分析
本场景主要介绍基于海量日志数据进行简单用户画像分析为背景,如何通过使用DataWorks完成数据采集 、加工数据、配置数据质量监控和数据可视化展现等任务。
SaaS 模式云数据仓库必修课
本课程由阿里云开发者社区和阿里云大数据团队共同出品,是SaaS模式云原生数据仓库领导者MaxCompute核心课程。本课程由阿里云资深产品和技术专家们从概念到方法,从场景到实践,体系化的将阿里巴巴飞天大数据平台10多年的经过验证的方法与实践深入浅出的讲给开发者们。帮助大数据开发者快速了解并掌握SaaS模式的云原生的数据仓库,助力开发者学习了解先进的技术栈,并能在实际业务中敏捷的进行大数据分析,赋能企业业务。 通过本课程可以了解SaaS模式云原生数据仓库领导者MaxCompute核心功能及典型适用场景,可应用MaxCompute实现数仓搭建,快速进行大数据分析。适合大数据工程师、大数据分析师 大量数据需要处理、存储和管理,需要搭建数据仓库?学它! 没有足够人员和经验来运维大数据平台,不想自建IDC买机器,需要免运维的大数据平台?会SQL就等于会大数据?学它! 想知道大数据用得对不对,想用更少的钱得到持续演进的数仓能力?获得极致弹性的计算资源和更好的性能,以及持续保护数据安全的生产环境?学它! 想要获得灵活的分析能力,快速洞察数据规律特征?想要兼得数据湖的灵活性与数据仓库的成长性?学它! 出品人:阿里云大数据产品及研发团队专家 产品 MaxCompute 官网 https://www.aliyun.com/product/odps&nbsp;
目录
相关文章
|
4月前
|
运维 Shell Linux
Linux 之大数据定制篇-Shell 编程
Linux 之大数据定制篇-Shell 编程
123 0
|
分布式计算 大数据 Spark
大数据编程实验二:RDD编程
大数据编程实验,学习Spark的RDD基本操作及键值对操作以及使用RDD编程解决实际具体问题的方法。
734 0
大数据编程实验二:RDD编程
|
8月前
|
运维 Java 大数据
Linux 之大数据定制篇-Shell 编程
Linux 之大数据定制篇-Shell 编程
201 2
|
4月前
|
大数据 Python Windows
Python大数据之Python进阶(二)多任务编程-进程
Python大数据之Python进阶(二)多任务编程-进程
33 0
|
9月前
|
分布式计算 Java Hadoop
云计算与大数据实验五 MapReduce编程
云计算与大数据实验五 MapReduce编程
225 0
|
9月前
|
分布式计算 安全 Java
云计算与大数据实验四 HDFS编程
云计算与大数据实验四 HDFS编程
74 0
|
9月前
|
存储 大数据 API
大数据数据存储的分布式文件系统的HDFS的基本使用的对应的API编程接口
在 Hdfs 中,使用命令行接口可以方便地对数据进行操作。
76 0
|
监控 Java 大数据
大数据编程技术基础实验六:ZooKeeper实验——进程协作
大数据基础实验六,使用ZooKeeper了解并实践进程协作的操作。
184 0
大数据编程技术基础实验六:ZooKeeper实验——进程协作
|
分布式计算 资源调度 算法
【大数据计算】(三) MapReduce的安装和基础编程
目录 1.词频统计任务要求 1.1 MapReduce程序编写方法 1.1.1 编写Map处理逻辑 1.1.2 编写Reduce处理逻辑 1.1.3 编写main方法 2 完整的词频统计程序 3. 编译打包程序 3.1 使用命令行编译打包词频统计程序 3.2 使用IDEA编译打包词频统计程序 4. 运行程序 5. 编程题 5.1 根据附件的数据文件flow_data.dat , 编程完成下面需求: 5.2 附加题(选做) 6. 福利送书 最后
354 0
【大数据计算】(三) MapReduce的安装和基础编程
|
分布式计算 Java 大数据
【大数据计算】(二) HBase 的安装和基础编程
目录 1. 安装HBase 1.1 下载安装文件 1.2 配置环境变量 1.3 添加用户权限 1.4 查看HBase版本信息 2. HBase的配置 2.1 单机模式配置 2.1.1 配置hbase-env.sh文件 2.1.2 配置hbase-site.xml 2.1.3 启动Hbase 2.2 伪分布模式配置 2.2.1 配置hbase-site.xml 3. HBase常用的Shell命令 3.1 在HBase中创建表 3.2 添加数据 3.3 查看数据 3.4 删除数据 3.5 删除表 3.6 查询历史记录 3.7 退出HBase数据库 4. HBase编程实践 4.1 编程题 API
165 0
【大数据计算】(二) HBase 的安装和基础编程