数据结构

目录

数组

ArrayList

总结

链表

LinkedList

总结

栈和队列

队列

二叉树

二叉树

二叉搜索树

​编辑

平衡二叉搜索树

TreeSet

Comparable

哈希表

总结


数组

数组分为静态数组和动态数组两种,之前我们学习的数组就属于静态数组。

  • 静态数组:数组创建完成后大小固定,不能改变数组长度,无法动态增加或减少元素数量。

  • 动态数组:动态数组是一种可以自动扩展大小的数组,它可以在运行时动态地增加或减少元素的数量

我们之前说过,数组在内存中是一片连续空间。而且由于元素类型一致,所以数组中每个元素大小是一样的。这样就可以根据数组起始地址、元素大小、元素索引计算出任意元素的内存地址,实现快速随机访问(跳着访问)

ArrayList

在集合体系中,ArrayList底层就是数组,而且是动态数组。

  • ArrayList默认的容量为10,也就是初始数组大小

  • 当元素存不下时触发扩容,默认扩大为原来1.5

构造方法:

构造方法 描述
ArrayList() 构造一个初始容量为 10 的空列表。
ArrayList(int initialCapacity) 构造一个具有指定初始容量的空列表。
ArrayList(Collection<? extends E> c) 构造一个包含指定集合元素的列表,按照集合迭代器返回元素的顺序。

在我们创建ArrayList时,如果知道需要的大小最好写上这样不浪费空间。

java 复制代码
   public static void main(String[] args) {
        // 创建一个ArrayList对象,初始化容量为3
        List<String> list = new ArrayList<>(3);
        list.add("hello");
        list.add("world");
        list.add("java");
        System.out.println(list.size());

        // 也可以使用List.of创建只读集合
        List<String> list2 = List.of("hello", "world", "java");
        System.out.println(list2.size());
    }

总结

  • **随机访问快:**有索引,所以随机访问速度特别快

  • **增删元素慢:**由于增删涉及到扩容、元素拷贝,所以速度慢

  • 查找慢:根据元素值查找只能逐一遍历,效率比较低


链表

链表(Linked List) 在内存空间中是不连续的,由分散在内存中的一个个节点组成,元素就存储在节点之中。

根据节点指针域指向不同,链表可以分为三类:

  • 单向链表

  • 双向链表

  • 循环链表

单向链表:节点指针域中记录下一个节点的地址

双向链表:节点指针域中记录上一个节点、下一个节点的地址

双向链表既可以从头到尾遍历,也可以从尾到头遍历,更加灵活。缺点是会占用更多内存。

循环链表:尾节点的指针与记录头节点的地址,链表形成环状

循环链表因为是环形结构,所以可以从任一节点访问整个链表,往往用来处理大型数据集或者解决一些特殊问题。

LinkedList

List集合有一个子类LinkedList,底层采用双向链表结构。提供了很多从链表头、尾操作的方法:

返回值 方法 描述
void addFirst(E e) 添加元素到当前链表的开头
void addLast(E e) 添加元素到当前链表的末尾
E getFirst() 返回链表开头的元素
E getLast() 返回链表末尾的元素
E removeFirst() 移除并返回链表开头的元素
E removeLast() 移除并返回链表末尾的元素
java 复制代码
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("world");
        System.out.println(list);

        list.addFirst("hello");
        System.out.println(list);

        list.addLast("java");
        list.addLast("java");
        System.out.println(list);

        list.removeLast();
        System.out.println(list);
    }
java 复制代码
[world]
[hello, world]
[hello, world, java, java]
[hello, world, java]

总结

  • **随机访问慢:**在内存中是非连续空间,无法根据索引快速计算地址,所以随机访问慢

  • **增删元素块:**只需要改变节点中指针指向就能实现增删、没有元素拷贝,所以速度块

  • 查找慢:根据元素值查找只能逐一遍历,效率比较低


栈和队列

队列

**队列(Queue)**是一组线性数据集合,要求从一端添加数据,从另一端移除数据,实现先进先出的效果。

类似于人们排队买票:

买票必须排队,队列有一个入口,一个出口。

  • 先来的人在队首,先买票

  • 后来的人在队尾,后买票

这叫做先进先出,这也是队列这种数据结构的特点。

这种先进先出的特性非常适合用在任务处理的业务中。比如:电商行业的秒杀业务,秒杀商品有限,肯定是先来先得。这个时候就可以用队列,无数的用户下单抢购,系统就可以把参与秒杀的人放入队列中,按照下单的顺序,先到先得。

队列数据结构总结:

  • 结构特征:入口出口不在一起

  • 效果:先进先出

  • 应用场景:有序任务处理

(Stack)同样是一种操作受限的线性数据结构。但与队列恰好相反,栈要求在同一端添加和删除数据。也就是说入口和出口是在一起的。

比如,浏览器有历史记录功能,你浏览过的所有页面都会添加到浏览历史中。如果浏览历史页面放入一个集合,我们肯定希望满足下面的要求:

  • 最先浏览的页面在集合的最底部

  • 最近浏览的页面在集合的最上面

这样,当我点击后退一步时,就能找到最近一次访问的页面了:

浏览器的历史记录存取页面数据的过程,刚好就是**先进后出,**完全符合栈的特征。

栈数据结构总结:

  • 结构:入口和出口在一起

  • 特点:先进后出


二叉树

前面我们学习的数据结构都是线性数据结构,即所有数据可以排成一条线,不会出现分叉。所以元素之间只会有前后关系,数据遍历也只有正序和倒序两种。

是一种非线性数据结构,数据之间不一定能排成一条线,而是像大树一样会出现分叉。只不过树这种结构是倒过来的,根在上,叶子在下:

虽然B是A的子节点,但是B也有自己的子节点D、E、F,所以B、D、E、F又形成了一棵树,它们是A的左子树。同理,C、G、H形成了A的右子树

另外,我们把节点到叶子节点的最长路径(边数)称为节点高度。例如:

  • 节点A的高度就是3

  • 节点B的高度是2

  • 节点D的高度是1

  • 节点I是叶子节点,高度为0

二叉树

树的结构由很多种,但最常用的还是二叉树Binary Tree)。顾名思义,二叉树的每个节点最多有两个叉,也就是两个子节点,分别是左子节点右子节点

注意,这里是说最多两个子节点,也有可能只有一个子节点。比如上图中的第一棵树的B节点,只有左子节点。

二叉搜索树

二叉搜索树 (Binary Search Tree)是一种特殊的二叉树,顾名思义,二叉搜索树是为了实现快速搜索而生的。

例如,我们要在上图第二棵树中查找是否存在12这个元素,步骤如下:

  • 与根节点比较,小于根就去找根的左子树,大于根就去找根的右子树。12小于21,找左子树,找到节点8

  • 与子树的父节点比较,小于父节点就去找左子树,大于父节点就去找右子树。12大于8,找右子树,找到节点10

  • 与子树的父节点比较,小于父节点就去找左子树,大于父节点就去找右子树。12大于10,找右子树,找到12了

树中有8个数据,但是我们只需要4次比较我们就找到了。如果是一个线性结构,比如链表,最差的情况下,你需要8次才能找到。这就是二叉搜索树的威力。

另外,二叉搜索树的这种特点也保证了树中的元素是经过排序的,使用合适的遍历方式,可以确保元素按照从小到大顺序依次遍历。

平衡二叉搜索树

虽然二叉搜索树在多数情况下效率都很高,但是也有极端情况。比如下图左边的二叉树:

由于数据不均衡,左边的二叉树已经退化成了链表,查找的效率大大下降。

平衡二叉搜索树就是为了解决这一问题。平衡二叉搜索要求:二叉树中任意一个节点的左右子树的高度相差不能大于 1

比如上图中:

  • 左边二叉树根节点的左子树高度是4,右子树高度是0,相差太大了。是非平衡二叉树

  • 右边二叉树根节点左子树高度为1,右子树高度也是1,左右平衡,是平衡二叉树

平衡二叉树并不是说绝对的平衡,而是尽量让左右子树相差不大,这样才能保证更好的节点增、删、改、查的效率。

平衡二叉搜索树中最知名的就是红黑树了,Java中的Set集合有一个实现类,叫做TreeSet,底层就是采用红黑树。

红黑树会左旋或者右旋来保证二叉树的平衡。


TreeSet

TreeSet首先是Set集合,所以具备Set集合的所有特点:存取无序(存和取的顺序不一致)、不允许重复、无索引。

但同时,TreeSet还具备二叉搜索树的特征,可以将存入的元素按照大小排序。

元素唯一、存取无顺序,元素会排序。

java 复制代码
import java.util.TreeSet;

public class TreeSetDemo {
    public static void main(String[] args) {
        sortInteger();
        sortString();
    }

    public static void sortInteger(){
        TreeSet<Integer> ts = new TreeSet<>();
        ts.add(10);
        ts.add(5);
        ts.add(20);
        ts.add(15);
        ts.add(30);
        // 数字按照大小排序
        System.out.println(ts); // [5, 10, 15, 20, 30]
    }

    public static void sortString(){
        TreeSet<String> ts2 = new TreeSet<>();
        ts2.add("hello");
        ts2.add("world");
        ts2.add("java");
        // 字符串同样可以排序,但是是按照字典顺序排序
        System.out.println(ts2); // [hello, java, world]
    }
}

Java中的TreeSet就是使用红黑树(自平衡二叉搜索树)结构实现的。因此可以对存入的元素排序,增删改查的效率都非常高。

TreeSet对元素排序是依据自然顺序排序,既:元素必须实现Comparable接口。

Comparable

Comparable,翻译过来就是可比较的,表示当前类型能够比较大小。

java 复制代码
public interface Comparable<T> {
    /**
     * 将this与指定的对象o进行比较以进行排序,返回值有三种情况:
     * - 大于0,表示 this > o
     * - 等于0,表示 this = o
     * - 小于0,表示 this < o
     */
    public int compareTo(T o);
}

compareTo方法就是用来比较大小的方法,将调用compareTo的当前对象(this)与参数中传入的对象(o)做比较,根据比较结果返回三种情况:

  • 大于0:说明this大于o

  • 等于0:说明this等于o

  • 小于0:说明this小于o

凡是实现Comarable接口 的类都需要实现compareTo方法,自定义比较逻辑。这就表示这个类有了比较的规则了,这个类就是可比较的。


哈希表

哈希表的英文是Hash Table,也叫散列表。

所谓的散列就是利用数组可以根据索引随机访问数据的特性,实现高效数据查询。因此散列表,或者哈希表底层就是基于数组来实现的

哈希表 根据键计算哈希值,决定了数据再数组中存储的位置索引。这样查询时也能根据键计算哈希值,得出数据的索引,快速查找出数据。这就将键(key)与值(value)之间形成了映射关系,具备了根据键高效查询值的能力

将学号通过哈希运算得到数组的索引,再根据索引对应的位置进行存储。但是存在一种情况,通过学号进行哈希运算得到的结果是一样的。这也就是哈希冲突。

当元素数量超过数组大小时,不管如何优化算法,总会出现两个不同的key计算出了相同hash值 的情况,这样两个数据就需要存储到数组的同一个索引位置。这也就是哈希冲突。

一个好的哈希函数要尽可能做到两点:

  • 如果key1 = key2,那么两者的哈希值要尽量一样

  • 如果key1 ≠ key2,那么两者的哈希值要尽量不同

这样才能确保不同的数据计算出的数组索引不一样。

哈希冲突不能解决只能尽量避免。例如扩大数组。挂链表、红黑树。

解决哈希冲突的手段主要是两点:

链表法

  • 当发生哈希冲突时,先检查两个key是否相同(利用equals方法比较key)

    • 如果key相同:说明是key有了新的值,覆盖旧值即可

    • 如果key不同:说明是冲突,把两个值形成链表,存入数组中同一个位置

在JDK8之后,当哈希表中某个链表长度超过阈值(8)时,会将链表转为红黑树以提高查询性能:

当哈希表的装载因子 超过阈值(0.75)时,就会触发动态扩容,哈希表的数组长度会扩大为原来2倍,然后将所有数据重新计算hash值,写入新的数组,以减少哈希冲突:

动态扩容

  • 我们把元素个数与数组长度的比值称为装载因子(load factor)

  • 理论上装载因子越小,出现哈希冲突的概率就越低,我们要保证哈希表的装载因子不超过某个阈值

  • 所以,当装载因子超过阈值时(Java中是0.75),就触发扩容,增大数组容量

总结

1、哈希表底层是一个数组,数组有一个初始容量capicity

2、对key做hash运算,对capacity取余,运算结果作为索引

3、将键值对存入数组中索引位置

4、如果该位置已经存在元素,则比较key是否一样

5、一样,则覆盖旧值;不一样,则形成链表

6、当链表长度超过阈值,转为红黑树,小于阈值,再转为链表

7、哈希表元素达到阈值 ,则对哈希表做扩容,元素做rehash

HashMap初始值是16

有人会问,如果key是字符串怎么办,我们要清楚计算机都是通过数字来进行存储的,在java中我们通过hashcode来进行计算出一个数字来进行接下来的流程。

由于HashMap底层会调用key的hashCode方法计算哈希值,调用key的equals方法比较key是否一致。而Object中hashCodeequals的默认实现与对象的属性值无关,不符合需求。

因此作为key的类型必须重写ObjecthashCodeequals方法,基于对象的属性来实现hashCodeequals方法。否则出现哈希冲突时将无法正确比较两个key是否一致。

HashSet底层则是基于HashMap来实现的。不过HashSet是单列集合,没有key/value之分,元素会作为key存储,value都是null。

就以Java中的HashSet为例来总结下哈希表的基本原理(对上述内容更细致的解释):

  • 哈希表底层是一个数组,数组有一个初始容量,叫做capacity(默认是16)

  • 存储元素时:

    • 对key做hash运算(基于key的hashCode方法),再对capacity取余作为在数组中的索引

    • 找到数组中索引位置,判断是否有元素

      • 没有元素:直接存储

      • 有元素:比较key值是否一致(基于key的equals方法)

        • 一致:则覆盖旧数据

        • 不一致:新加入的元素与之前元素形成链表

    • 当链表长度超过阈值(8),则把链表转为红黑树;红黑树元素小于阈值(6),再转为链表

    • 当哈希加载因子达到阈值(0.75),则对哈希表扩容,元素做rehash。

相关推荐
浅念-8 小时前
Linux 开发环境与工具链
linux·运维·服务器·数据结构·c++·经验分享
报错小能手11 小时前
深入理解 Linux 物理内存管理
学习·操作系统
zx_zx_12311 小时前
哈希表的学习
学习·哈希算法·散列表
FAFU_kyp11 小时前
Kimi Coding Plan API 集成问题与解决方案
学习
x_xbx12 小时前
LeetCode:34. 在排序数组中查找元素的第一个和最后一个位置
数据结构·算法·leetcode
菜菜小狗的学习笔记14 小时前
剑指Offer算法题(九)搜索
数据结构·算法·深度优先
L-影14 小时前
下篇:它到底是怎么操作的——AI中半监督学习的类型与作用,以及为什么它成了行业的“最优解”
人工智能·学习·机器学习·ai·半监督学习
xw-busy-code15 小时前
抽象语法书学习笔记
笔记·学习·ast·抽象语法树