Java数据结构基础知识必知必会

1. 数据结构概述

Java的集合框架其实就是对数据结构的封装,在学习集合框架之前,有必要先了解下数据结构。

1.1. 什么是数据结构(了解)

所谓数据结构,其实就是计算机存储、组织数据的方式。

数据结构是用来模拟数据存储操作的,其实就是对数据做增删改查操作。

  • 增:把某个数据存储到某个容器中

  • 删:从容器中把某个数据删除掉

  • 改:把容器中某个数据替换成另一个数据

  • 查:把容器中的数据查询出来

不同的数据结构,底层采用不同的存储方式(算法),在具体操作的时候效率是不一样的,比如有的查询速度很快,有的插入速度很快,有的操作头和尾速度很快等。

常见的数据结构:

  • 数组(Array) 掌握

  • 链表(Linked List) 了解

  • 哈希表(Hash) 了解

  • 栈(Stack) 了解

  • 队列(Queue) 了解

  • 树(Tree) 了解

  • 图(Graph)

  • 堆(Heap)

1.2. 数组结构

1.2.1. 模拟ArrayList(掌握)

假设我现在是某个篮球队的教练,需要安排5个球员上场打球。此时需要模拟上场球员的存储,简单一点,我们就只存储上场球员的球衣号码。那么此时我需要以下几个操作:

1.初始一个容量为5的容器,用来存储场上的5个球衣号码。

2.安排5个球员上场,比如球员号码分别为11、22、33、44、55。

3.查询指定索引位置球员的球衣号码是多少,如查询索引位置为2的球衣号码是33。

4.替换场上索引位置为2的球员,使用333号替换33号。

5.罚下场上索引位置为2的球员(直接罚下,没有补位)。

6.打印出场上球员的球衣号码,打印风格如 [11,22,33,44,55]。

操作前后效果图:

image.png

1.2.2. 初始化操作(掌握)

使用Integer数组来存储场上球员号码,提供了两个构造器,一个用于自定义初始化容量,一个用于使用默认的初始化容量10。

public class PlayerList {

    //存储场上球员号码

    private Integer[] players = null;

    //存储场上球员数量

    private int size = 0;

    //自定义初始容量

    public PlayerList(int initialCapacity) {

        if (initialCapacity < 0) {

            throw new RuntimeException("初始容量不能为负数");

        }

        this.players = new Integer[initialCapacity];

    }

    //默认初始容量为10

    public PlayerList() {

        this(10);

    }

}

测试代码:

public class App {

    public static void main(String[] args) {

        //初始容量为默认的10

        PlayerList list1 = new PlayerList();

        //自定义初始化容量为5

        PlayerList list2 = new PlayerList(5);

        //自定义初始化容量为20

        PlayerList list3 = new PlayerList(20);

    }

}

1.2.3. 打印操作(掌握)

public String toString() {

    if (players == null) {//如果没有初始化容器

        return "null";

    }
    
    if (size == 0) {//如果容器中球员数量为0

        return "[]";

    }

    StringBuilder sb = new StringBuilder(40);

    sb.append("[");

    for (int index = 0; index < size; index++) {

        sb.append(players[index]);

        //如果不是最后一个

        if (index != size - 1) {

            sb.append(",");

        } else {//如果是最后一个

            sb.append("]");

        }

    }

    return sb.toString();

}

1.2.4. 保存操作(掌握)

//向场上添加一个球员号码

public void add(Integer playerNumber) {

    this.players[size] = playerNumber;//保存球衣号码

    size++;//场上球员数量加1

}

测试代码

public class App {

    public static void main(String[] args) {

        //初始化容器,设置初始化容量为5

        PlayerList list = new PlayerList(5);

        System.out.println(list);

        //向容器中添加5个元素(球员号码)

        list.add(11);

        list.add(22);

        list.add(33);

        list.add(44);

        list.add(55);

        //打印容器中每一个元素

        System.out.println(list);

    }

}

输出结果:

[]

[11,22,33,44,55]

因为数组的长度是固定的,此时的players数组只能存储5个元素,如果再多存储一个就报错:数组索引越界。此时就要考虑在保存操作时对数组做扩容操作,扩容的原理是:

  • 创建一个原数组长度两倍长的新数组

  • 把旧数组中的所有元素拷贝到新数组中

  • 把新数组的引用赋给旧数组变量

保存操作时扩容操作

//向场上添加一个球员号码

public void add(Integer playerNumber) {

    //如果容器容量已满,此时需要扩容,此时扩容机制为原来容量的2倍

    if (size == players.length) {

        this.players = Arrays.copyOf(players, size * 2);

    }

    //-----------------------------------------

    this.players[size] = playerNumber;//保存球衣号码

    size++;//场上球员数量加1

}

1.2.5. 查询操作(掌握)

需求:查询指定索引位置球员的球衣号码是多少,如查询索引位置为2的球衣号码是33。

其实就是返回数组中,指定索引对应的元素值。

public Integer get(int index) {

    if (index < 0 || index >= size) {

        throw new RuntimeException("索引越界");

    }

    return players[index];

}

测试代码:

Integer playerNum = list.get(2);

System.out.println(playerNum);

输出结果:

33

1.2.6. 修改操作(掌握)

需求:替换场上索引位置为2的球员,使用333号替换33号。

//替换指定位置的球员号码

public void set(int index, Integer newPlayerNumber) {

    if (index < 0 || index >= size) {

        throw new RuntimeException("索引越界");

    }

    players[index] = newPlayerNumber;

}

1.2.7. 删除操作(掌握)

需求:罚下场上索引位置为2的球员(直接罚下,没有补位)。

删除操作的原理,把后续的元素整体往前挪动一个位置。

//删除指定位置的球员号码

public void remove(int index) {

    if (index < 0 || index >= size) {

        throw new RuntimeException("索引越界");

    }

    for (int i = index; i < size - 1; i++) {

        players[i] = players[i + 1];

    }

    players[size - 1] = null;

    size--;

}

1.2.8. 让容器支持存储任意数据类型的元素(掌握)

此时元素类型是Integer类型,也就是只能存储整型的数据,但是却不能存储其他类型的数据,此时我们可以考虑吧元素类型改成Object,那么Object数组可以存储任意类型的数据。

public class MyArrayList {

    //存储元素

    private Object[] elementData = null;

    //存储元素数量

    private int size = 0;

    //自定义初始容量

    public MyArrayList(int initialCapacity) {

        if (initialCapacity < 0) {

            throw new RuntimeException("初始容量不能为负数");

        }

        this.elementData = new Object[initialCapacity];

    }

    //默认初始容量为10

    public MyArrayList() {

        this(10);

    }

    //向容器中添加一个元素

    public void add(Object playerNumber) {

        //如果容器容量已满,此时需要扩容,此时扩容机制为原来容量的2倍

        if (size == elementData.length) {

            this.elementData = Arrays.copyOf(elementData, size * 2);

        }

        //-----------------------------------------

        this.elementData[size] = playerNumber;//保存球衣号码

        size++;//容器中元素数量加1

    }

    //查询指定位置的元素

    public Object get(int index) {

        if (index < 0 || index >= size) {

            throw new RuntimeException("索引越界");

        }

        return elementData[index];

    }

    //替换指定索引位置的元素

    public void set(int index, Object newPlayerNumber) {

        if (index < 0 || index >= size) {

            throw new RuntimeException("索引越界");

        }

        elementData[index] = newPlayerNumber;

    }

    //删除指定索引位置的元素

    public void remove(int index) {

        if (index < 0 || index >= size) {

            throw new RuntimeException("索引越界");

        }

        for (int i = index; i < size - 1; i++) {

            elementData[i] = elementData[i + 1];

        }

        elementData[size - 1] = null;

        size--;

    }

    public String toString() {

        if (elementData == null) {//如果没有初始化容器

            return "null";

        }

        if (size == 0) {//如果容器中元素数量为0

            return "[]";

        }

        StringBuilder sb = new StringBuilder(40);

        sb.append("[");

        for (int index = 0; index < size; index++) {

            sb.append(elementData[index]);

            //如果不是最后一个

            if (index != size - 1) {

                sb.append(",");

            } else {//如果是最后一个

                sb.append("]");

            }

        }

        return sb.toString();

    }

}

1.2.9. 数组的性能分析(了解)

在计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间,常用大O符号来表述。

时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。

我们在这里针对ArrayList存储数据的增删改查(CRUD)做性能分析:

  • 保存操作:

如果保存在数组的最后一个位置,至少需要操作一次。

如果保存在数组的第一个位置,如果存在N个元素,此时需要操作N次(后面的元素要整体后移)。

平均: (N+1) /2 次。 N表示数组中元素的个数。 如果要扩容,更慢,性能更低。

  • 删除操作:

如果删除最后一个元素,操作一次。

如果删除第一个元素,操作N次。

平均:(N+1)/2次.

  • 修改操作: 操作1次.

  • 查询操作:根据索引查询元素: 操作1次.

结论:基于数组的数据结构做查询是和修改是非常快的,做保存和删除操作比较慢了。

那如果想保证保存和删除操作的性能,此时就得提提链表这种数据结构了。

1.3. 其他数据结构(了解)

1.3.1. 链表(了解)

链表结构(火车和火车车厢):

1):单向链表,只能从头遍历到尾/只能从尾遍历到头。

2):双向链表,既可以从头遍历到尾,又可以从尾遍历到头。

通过引用来表示上一个节点和下一个节点的关系。

单向链表:

image.png

双向链表:

image.png

对LinekdList操作的性能分析:

双向链表可以直接获取自己的第一个和最后一个节点。

  • 保存操作

如果新增的元素在第一个或最后一个位置,那么操作只有1次。

  • 删除操作

如果删除第一个元素 :操作一次

如果删除最后一个元素:操作一次

如果删除中间的元素:

找到元素节点平均操作:(1+N)/2次

找到节点之后做删除操作: 1次

  • 修改操作

平均:(N+1)/2次

  • 查询操作:

平均:(N+1)/2次

结论:

ArrayList: 查询、更改较快,新增和删除较慢。

LinkedList: 查询、更改较慢,新增和删除较快。

一般的,在开发中数据都是存储在数据库中,我们一般主要用来查询,所以ArrayList使用较多。

1.3.2. 队列(了解)

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。

进行插入操作的端称为队尾,进行删除操作的端称为队头。

单向队列(Queue):先进先出(FIFO),只能从队列尾插入数据,只能从队列头删除数据。

image.png

双向队列(Deque):可以从队列尾/头插入数据,只能从队列头/尾删除数据。

image.png

结论:最擅长操作头和尾。

1.3.3. 栈(了解)

栈(stack)又名堆栈,它是一种运算受限的线性表,后进先出(LIFO),和手枪弹夹类似。

image.png
image.png

栈结构仅允许在表的一端进行插入和删除运算,这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈中插入新元素又称作入栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素。从一个栈中删除元素又称作出栈,表示把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素(LIFO)。

1.3.4. 哈希表(了解)

一般的,数组中元素在数组中的索引位置是随机的,元素的取值和元素的位置之间不存在确定的对应关系。因此,在数组中查找特定的值时,需要把查找值和一系列的元素进行比较。

image.png

此时的查询效率依赖于查找过程中所进行的比较次数,如果比较次数较多,查询效率还是不高。

如果元素的值(value)和在数组中的索引位置(index)有一个确定的对应关系,我们把这种关系称之为哈希(hash)。则元素值和索引对应的公式为: index = hash(value)。也就是说,通过给定元素值,只要调用hash(value)方法,就能找到数组中取值为value的元素的位置。


image.png

比如,图中的hash算法公式为:index = value / 10 - 1。

在往哈希表中存储对象时,该hash算法就是对象的hashCode方法。

注:这里仅仅是假设算法公式是这样的,真实的算法公式我们可以不关心。

1.3.5. 树和二叉树(了解)

前面我们介绍的数据结构数组、栈、队列,链表都是线性数据结构,除此之外还有一种比较复杂的数据结构——树。

计算机中的树,是根据生活中的树抽象而来的,表示N个有父子关系的节点的集合。

  • N为0的时候,该节点集合为空,这棵树就是空树

  • 任何非空树中,有且只有一个根节点(root)

  • N>1时,一颗树由根和若干棵子树组成,每棵子树由更小的若干子树组成

树中的节点根据有没有子节点,分成两种:

  • 普通节点:拥有子节点的节点。

  • 叶子节点:没有字节点的节点。

image.png

二叉树:一种特殊的,遵循某种规则的树

树的结构因为存在多种子节点情况,真的太复杂了,如果我们对普通的树加上一些约束,比如让每一棵树的节点最多只能包含两个子节点,而且严格区分左子节点和右子节点(左右位置不能交换),此时就形成了二叉树。

排序二叉树,有顺序的树:

  • 若左子树不为空,则左子树所有节点的值小于根节点的值。

  • 若右子树不为空,则右子树所有节点的值大于根节点的值。

  • 左右子树也分别是排序二叉树。

image.png

红黑树:更高查询效率的的排序二叉树。

排序二叉树可以快速查找,但是如果只有左节点或者左右右节点的时候,此时二叉树就变成了普通的链表结构,查询效率比较低。为此一种更高效的二叉树出现了——红黑树。

  • 每个节点要么是红色的,要么是黑色的。

  • 根节点永远是黑色的。

  • 所有叶子节点都是空节点(null),是黑色的。

  • 每个红色节点的两个子节点都是黑色的。

  • 从任何一个节点到其子树每个叶子节点的路径都包含相同数量的黑色节点。

image.png

若要获得最好的学习效果,需要配合对应教学视频一起学习。需要完整教学视频,请参看https://ke.qq.com/course/272077

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,740评论 0 13
  • 1 序 2016年6月25日夜,帝都,天下着大雨,拖着行李箱和同学在校门口照了最后一张合照,搬离寝室打车去了提前租...
    RichardJieChen阅读 5,088评论 0 12
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,916评论 2 89
  • 早晨在阳台做伸展运动的时候,看到猫咪们集体在仰头朝上望,顺着这群虎视眈眈的眼神看去,发现我的水仙花开了,一下子开了...
    Jessy自由行走的猫阅读 487评论 0 3
  • 1.我认为印象最深刻的三部分 a.分工合作,可以达到真正提高效率的目的. 自从上大学之后,合作意识在我们身上得到了...
    初见_86a6阅读 144评论 0 0