java 中常见的数据结构:数组、栈、队列、链表、图、哈希表、树、堆
1.jpg

数组

特点:数据是连续的;随机访问速度快
缺点:数组的大小在创建后就确定了,无法扩容;数组只能存储一种类型的数据
数组中复杂一点的是多维数组和动态数组。Java 的 Collection 集合中提供了 ArrayList 和 Vector 来实现动态数组。

数组的下标寻址十分迅速,但计算机的内存是有限的,故数组的长度也是有限的,实际应用当中的数据往往十分庞大;而且无序数组的查找最坏情况需要遍历整个数组;后来人们提出了二分查找,二分查找要求数组的构造一定有序,二分法查找解决了普通数组查找复杂度过高的问题。任何一种数组无法解决的问题就是插入、删除操作比较复杂,因此,在一个增删查改比较频繁的数据结构中,数组不会被优先考虑。

链表

链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node) 的引用,该节点还有一个元素和一个指向另一条链表的引用

单向链表

单向链表是链表的一种,它由节点组成,每个节点都包含下一个节点的指针
2.jpg
单链表的特点:节点的链接方向是单向的;相对于数组来说,单链表的的随机访问速度较慢,但是单链表删除/添加数据的效率很高。

双向链表

双向链表是链表的一种。双链表也是由节点组成,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。

链表的优缺点

由于不必按照顺序的方式存储,链表在插入、删除的时候可以达到 O(1) 的时间复杂度(只需要重新指向引用即可,不需要像数组那样移动其他元素)。除此之外,链表还克服了数组必须预先知道数据大小的缺点,从而可以实现灵活的内存动态管理

优点

  • 不需要初始化容量
  • 可以添加任意元素
  • 插入和删除的时候只需要更新引用,时间复杂度 O(1)

    缺点

  • 含有大量的引用,占用的内存空间大

  • 查找元素需要遍历整个链表,耗时(不适用于查找)

    Java 实现双向链表

    ```java package structure;

/**

  • Java 实现的双向链表。
  • 注:java自带的集合包中有实现双向链表,路径是:java.util.LinkedList */ public class DoubleLink {

    // 表头 private DNode mHead; // 节点个数 private int mCount;

    // 双向链表“节点”对应的结构体 private class DNode {

    1. public DNode prev;
    2. public DNode next;
    3. public T value;
    4. public DNode(T value, DNode prev, DNode next) {
    5. this.value = value;
    6. this.prev = prev;
    7. this.next = next;
    8. }

    }

    // 构造函数 public DoubleLink() {

    1. // 创建“表头”。注意:表头没有存储数据!
    2. mHead = new DNode<T>(null, null, null);
    3. mHead.prev = mHead.next = mHead;
    4. // 初始化“节点个数”为0
    5. mCount = 0;

    }

    // 返回节点数目 public int size() {

    1. return mCount;

    }

    // 返回链表是否为空 public boolean isEmpty() {

    1. return mCount==0;

    }

    // 获取第index位置的节点 private DNode getNode(int index) {

    1. if (index<0 || index>=mCount)
    2. throw new IndexOutOfBoundsException();
    3. // 正向查找
    4. if (index <= mCount/2) {
    5. DNode<T> node = mHead.next;
    6. for (int i=0; i<index; i++)
    7. node = node.next;
    8. return node;
    9. }
    10. // 反向查找
    11. DNode<T> rnode = mHead.prev;
    12. int rindex = mCount - index -1;
    13. for (int j=0; j<rindex; j++)
    14. rnode = rnode.prev;
    15. return rnode;

    }

    // 获取第index位置的节点的值 public T get(int index) {

    1. return getNode(index).value;

    }

    // 获取第1个节点的值 public T getFirst() {

    1. return getNode(0).value;

    }

    // 获取最后一个节点的值 public T getLast() {

    1. return getNode(mCount-1).value;

    }

    // 将节点插入到第index位置之前 public void insert(int index, T t) {

    1. if (index==0) {
    2. DNode<T> node = new DNode<T>(t, mHead, mHead.next);
    3. mHead.next.prev = node;
    4. mHead.next = node;
    5. mCount++;
    6. return ;
    7. }
    8. DNode<T> inode = getNode(index);
    9. DNode<T> tnode = new DNode<T>(t, inode.prev, inode);
    10. inode.prev.next = tnode;
    11. inode.next = tnode;
    12. mCount++;
    13. return ;

    }

    // 将节点插入第一个节点处。 public void insertFirst(T t) {

    1. insert(0, t);

    }

    // 将节点追加到链表的末尾 public void appendLast(T t) {

    1. DNode<T> node = new DNode<T>(t, mHead.prev, mHead);
    2. mHead.prev.next = node;
    3. mHead.prev = node;
    4. mCount++;

    }

    // 删除index位置的节点 public void del(int index) {

    1. DNode<T> inode = getNode(index);
    2. inode.prev.next = inode.next;
    3. inode.next.prev = inode.prev;
    4. inode = null;
    5. mCount--;

    }

    // 删除第一个节点 public void deleteFirst() {

    1. del(0);

    }

    // 删除最后一个节点 public void deleteLast() {

    1. del(mCount-1);

    } } ```

    栈的介绍

    栈是一种线性存储结构,它有以下几个特点:

  • 栈中数据是按照”后进先出“(LIFO, Last In First Out)方式进出栈的
  • 向栈中添加/删除数据时,只能从栈顶进行操作

栈通常包括的三种操作:push、peek、pop

  • push — 向栈中添加元素
  • peek — 返回栈顶元素
  • pop — 返回并删除栈顶元素的操作

3.jpg

栈的 Java 实现

Java 中有两种方式可以实现栈

  1. 数组实现的栈,能存储任意类型的数据
  2. Java 的 Collection集合 中自带的”栈”(stack) 的示例

    用数组实现

    ```java package structure;

import java.lang.reflect.Array;

public class GeneralArrayStack {

  1. private static final int DEFAULT_SIZE = 12;
  2. private T[] mArray;
  3. private int count;
  4. public GeneralArrayStack(Class<T> type) {
  5. this(type, DEFAULT_SIZE);
  6. }
  7. public GeneralArrayStack(Class<T> type, int size) {
  8. // 不能直接使用mArray = new T[DEFAULT_SIZE];
  9. mArray = (T[]) Array.newInstance(type, size);
  10. count = 0;
  11. }
  12. // 将val添加到栈中
  13. public void push(T val) {
  14. mArray[count++] = val;
  15. }
  16. // 返回“栈顶元素值”
  17. public T peek() {
  18. return mArray[count-1];
  19. }
  20. // 返回“栈顶元素值”,并删除“栈顶元素”
  21. public T pop() {
  22. T ret = mArray[count-1];
  23. count--;
  24. return ret;
  25. }
  26. // 返回“栈”的大小
  27. public int size() {
  28. return count;
  29. }
  30. // 返回“栈”是否为空
  31. public boolean isEmpty() {
  32. return size()==0;
  33. }
  34. // 打印“栈”
  35. public void PrintArrayStack() {
  36. if (isEmpty()) {
  37. System.out.printf("stack is Empty\n");
  38. }
  39. System.out.printf("stack size()=%d\n", size());
  40. int i=size()-1;
  41. while (i>=0) {
  42. System.out.println(mArray[i]);
  43. i--;
  44. }
  45. }
  46. public static void main(String[] args) {
  47. String tmp;
  48. GeneralArrayStack<String> astack = new GeneralArrayStack<String>(String.class);
  49. // 将10, 20, 30 依次推入栈中
  50. astack.push("10");
  51. astack.push("20");
  52. astack.push("30");
  53. // 将“栈顶元素”赋值给tmp,并删除“栈顶元素”
  54. tmp = astack.pop();
  55. System.out.println("tmp="+tmp);
  56. // 只将“栈顶”赋值给tmp,不删除该元素.
  57. tmp = astack.peek();
  58. System.out.println("tmp="+tmp);
  59. astack.push("40");
  60. astack.PrintArrayStack(); // 打印栈
  61. }

}

  1. <a name="aUayr"></a>
  2. #### Collection 集合自带的栈实现
  3. ```java
  4. package structure;
  5. import java.util.Stack;
  6. public class StackTest {
  7. public static void main(String[] args) {
  8. int tmp=0;
  9. Stack<Integer> astack = new Stack<Integer>();
  10. // 将10, 20, 30 依次推入栈中
  11. astack.push(10);
  12. astack.push(20);
  13. astack.push(30);
  14. // 将“栈顶元素”赋值给tmp,并删除“栈顶元素”
  15. tmp = astack.pop();
  16. // 只将“栈顶”赋值给tmp,不删除该元素.
  17. tmp = (int)astack.peek();
  18. astack.push(40);
  19. while(!astack.empty()) {
  20. tmp = (int)astack.pop();
  21. System.out.printf("tmp=%d\n", tmp);
  22. }
  23. }
  24. }

队列

队列的介绍

队列是一种线性存储结构,它有以下几个特点:

  • 队列中数据是按照”先进先出“(FIFO, First-In-First-Out)方式进出队列的
  • 队列只允许在”队首”进行删除操作,而在”队尾”进行插入操作

队列通常包括的两种操作:入队列 和 出队列。
4.jpg

队列的 Java 实现

Java 中有两种方式可以实现队列:

  1. 数组实现的队列,能存储任意类型的数据
  2. Java 的 Collection 集合中自带的”队列”(LinkedList)的示例

    数组的实现

    ```java package structure;

// java : 数组实现“队列”,只能存储int数据。 public class ArrayQueue { private int[] mArray; private int mCount;

  1. public ArrayQueue(int sz){
  2. mArray = new int[sz];
  3. mCount = 0;
  4. }
  5. // 将val添加到队尾
  6. public void add(int val){
  7. mArray[mCount++] = val;
  8. }
  9. // 返回队首元素
  10. public int front(){
  11. return mArray[0];
  12. }
  13. // 返回队首元素的值,并且删除队首元素
  14. public int pop(){
  15. int ret = mArray[0];
  16. mCount--;
  17. for(int i = 1;i <= mCount;i++){
  18. mArray[i-1] = mArray[i];
  19. }
  20. return ret;
  21. }
  22. // 返回队列的大小
  23. public int size(){
  24. return mCount;
  25. }
  26. // 返回队列是否为空
  27. public boolean isEmpty(){
  28. return size() == 0;
  29. }
  30. public static void main(String[] args) {
  31. int tmp = 0;
  32. ArrayQueue astck = new ArrayQueue(12);
  33. // 将10、20、30依次入队列
  34. astck.add(10);
  35. astck.add(20);
  36. astck.add(30);
  37. // 打印队首元素并删除队首元素
  38. tmp = astck.pop();
  39. System.out.println(tmp);
  40. // 打印队首元素 但不删除
  41. tmp = astck.front();
  42. System.out.println(tmp);
  43. astck.add(40);
  44. while (!astck.isEmpty()) {
  45. System.out.printf("size()=%d\n", astck.pop());
  46. }
  47. }

}

  1. <a name="G7tSN"></a>
  2. ## 二叉查找树
  3. 二叉查找树(Binary Search Tree),又被称为二叉搜索树<br />它是特殊的二叉树,具有以下特点:
  4. - 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值
  5. - 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值
  6. - 任意节点的左、右子树也分别为二叉查找树
  7. - 没有键值相等的节点
  8. <a name="aib7a"></a>
  9. ### 二叉查找树的 java 实现
  10. 定义一个二叉查找树:
  11. ```java
  12. public class BSTree<T extends Comparable<T>> {
  13. private BSTNode<T> mRoot; // 根结点
  14. public class BSTNode<T extends Comparable<T>> {
  15. T key; // 关键字(键值)
  16. BSTNode<T> left; // 左孩子
  17. BSTNode<T> right; // 右孩子
  18. BSTNode<T> parent; // 父结点
  19. public BSTNode(T key, BSTNode<T> parent, BSTNode<T> left, BSTNode<T> right) {
  20. this.key = key;
  21. this.parent = parent;
  22. this.left = left;
  23. this.right = right;
  24. }
  25. }
  26. ......
  27. }

前序遍历

若二叉树非空,则执行以下操作:

  1. 访问根结点
  2. 先序遍历左子树
  3. 先序遍历右子树 ```java // 前序遍历 private void preOrder(BSTNode tree) { if(tree != null) {
    1. System.out.print(tree.key + " ");
    2. preOrder(tree.left);
    3. preOrder(tree.right);
    } }

public void preOrder() { preOrder(mRoot); }

  1. <a name="QNjDb"></a>
  2. ### 中序遍历
  3. 若二叉树非空,则执行以下操作:
  4. 1. 中序遍历左子树
  5. 2. 访问根结点
  6. 3. 中序遍历右子树
  7. ```java
  8. // 中序遍历
  9. private void inOrder(BSTNode<T> tree) {
  10. if(tree != null) {
  11. inOrder(tree.left);
  12. System.out.print(tree.key+" ");
  13. inOrder(tree.right);
  14. }
  15. }
  16. public void inOrder() {
  17. inOrder(mRoot);
  18. }

后序遍历

若二叉树非空,则执行以下操作:

  1. 后序遍历左子树
  2. 后序遍历右子树
  3. 访问根结点 ```java // 后序遍历 private void postOrder(BSTNode tree) { if(tree != null) {
    1. postOrder(tree.left);
    2. postOrder(tree.right);
    3. System.out.print(tree.key+" ");
    } }

public void postOrder() { postOrder(mRoot); }

  1. ![5.jpg](https://cdn.nlark.com/yuque/0/2022/jpeg/22885189/1649657468159-e82f6a5f-9281-4d9c-8827-0715eef8a9a5.jpeg#clientId=ucc0a9937-d37a-4&crop=0&crop=0&crop=1&crop=1&from=ui&height=301&id=ud463faee&margin=%5Bobject%20Object%5D&name=5.jpg&originHeight=401&originWidth=683&originalType=binary&ratio=1&rotation=0&showTitle=false&size=69477&status=done&style=none&taskId=ue723420a-6644-4665-8662-c7be948ce1e&title=&width=512)<br />这颗树的前中后序的遍历结果:
  2. - 前序遍历结果:3 1 2 5 4 6
  3. - 中序遍历结果:1 2 3 4 5 6
  4. - 后序遍历结果:2 1 4 6 5 3
  5. <a name="d3SBT"></a>
  6. ### 查找键值为 key 的节点
  7. 递归实现:
  8. ```java
  9. // 查找二叉树中键值为key的节点
  10. private BSTNode<T> search(BSTNode<T> x, T key) {
  11. if (x==null)
  12. return x;
  13. int cmp = key.compareTo(x.key);
  14. if (cmp < 0)
  15. return search(x.left, key);
  16. else if (cmp > 0)
  17. return search(x.right, key);
  18. else
  19. return x;
  20. }
  21. public BSTNode<T> search(T key) {
  22. return search(mRoot, key);
  23. }

查找二叉树的最大节点

查找最大结点:返回 tree 为根结点的二叉树的最大结点

  1. private BSTNode<T> maximum(BSTNode<T> tree) {
  2. if (tree == null)
  3. return null;
  4. while(tree.right != null)
  5. tree = tree.right;
  6. return tree;
  7. }
  8. public T maximum() {
  9. BSTNode<T> p = maximum(mRoot);
  10. if (p != null)
  11. return p.key;
  12. return null;
  13. }

查找二叉树的最小节点

查找最小结点:返回 tree 为根结点的二叉树的最小结点

  1. private BSTNode<T> minimum(BSTNode<T> tree) {
  2. if (tree == null)
  3. return null;
  4. while(tree.left != null)
  5. tree = tree.left;
  6. return tree;
  7. }
  8. public T minimum() {
  9. BSTNode<T> p = minimum(mRoot);
  10. if (p != null)
  11. return p.key;
  12. return null;
  13. }

平衡二叉树(AVL 树)

AVL 树的特点:任何节点的两个子树的高度最大差别为1
6.jpg
平衡二叉树的难点在于,当删除或者增加节点的情况下,如何通过左旋或者右旋的方式来保持左右平衡。
Java 中最常见的平衡二叉树就是红黑树,节点是红色或者黑色,通过颜色的约束来维持着二叉树的平衡:

  1. 每个节点都只能是红色或者黑色
  2. 根节点是黑色
  3. 每个叶节点(NIL 节点,空节点)是黑色的。
  4. 如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点。
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

7.jpg

哈夫曼树

Huffman Tree,中文名是哈夫曼树或霍夫曼树,它是最优二叉树
定义:给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若树的带权路径长度达到最小,则这棵树被称为哈夫曼树
8.jpg
路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为 L-1
例子:100和80的路径长度是1,50 和 30 的路径长度是2,20 和 10 的路径长度是 3

结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
例子:节点 20 的路径长度是 3,它的带权路径长度 = 路径长度 权 = 3 20 = 60

树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL
例子:示例中,树的 WPL= 1100 + 250 + 320 + 310 = 100 + 100 + 60 + 30 = 290

二叉堆

二叉堆是完全二元树或者是近似完全二元树,按照数据的排列方式可以分为两种:最大堆和最小堆

  • 最大堆:父结点的键值总是大于或等于任何一个子节点的键值
  • 最小堆:父结点的键值总是小于或等于任何一个子节点的键值

二叉堆一般都通过”数组”来实现,下面是数组实现的最大堆和最小堆的示意图:
9.jpg

二叉堆的添加

假设在最大堆 [90,80,70,60,40,30,20,10,50] 种添加 85,需要执行的步骤如下:
10.jpg
当向最大堆中添加数据时:

  1. 先将数据加入到最大堆的最后
  2. 然后尽可能把这个元素往上挪,直到挪不动为止

将 85 添加到 [90,80,70,60,40,30,20,10,50] 中后,最大堆变成了 [90,85,70,60,80,30,20,10,50,40]

二叉堆的删除

假设从最大堆 [90,85,70,60,80,30,20,10,50,40] 中删除 90,需要执行的步骤如下:
11.jpg
如上图所示,当从最大堆中删除数据时:先删除该数据,然后用最大堆中最后一个的元素插入这个空位;接着,把这个“空位”尽量往上挪,直到剩余的数据变成一个最大堆。
从 [90,85,70,60,80,30,20,10,50,40] 删除 90 之后,最大堆变成了 [85,80,70,60,40,30,20,10,50]

图(graph) 是由一些点(vertex) 和这些点之间的连线(edge) 所组成的;其中,点通常被成为”顶点(vertex)”,而点与点之间的连线则被成为”边或弧”(edege)。通常记为,G=(V,E)。
根据边是否有方向,将图可以划分为:无向图和有向图

邻接点和度

邻接点:一条边上的两个顶点叫做邻接点。在有向图中,除了邻接点之外;还有”入边”和”出边”的概念
度:在无向图中,某个顶点的度是邻接到该顶点的边(或弧)的数目。在有向图中,度还有”入度”和”出度”之分

路径和回路

路径:如果顶点(Vm) 到顶点(Vn) 之间存在一个顶点序列。则表示 Vm 到 Vn 是一条路径。
路径长度:路径中”边的数量”
简单路径:若一条路径上顶点不重复出现,则是简单路径
回路:若路径的第一个顶点和最后一个顶点相同,则是回路
简单回路:第一个顶点和最后一个顶点相同,其它各顶点都不重复的回路则是简单回路

连通图和连通分量

连通图:对无向图而言,任意两个顶点之间都存在一条无向路径,则称该无向图为连通图。 对有向图而言,若图中任意两个顶点之间都存在一条有向路径,则称该有向图为强连通图
连通分量:非连通图中的各个连通子图称为该图的连通分量

图的存储结构

邻接矩阵:是指用矩阵来表示图。它是采用矩阵来描述图中顶点之间的关系(及弧或边的权)
邻接表:邻接表是图的一种链式存储表示方法。它是改进后的”邻接矩阵”,它的缺点是不方便判断两个顶点之间是否有边,但是相对邻接矩阵来说更省空间

图的深度优先搜索

图的深度优先搜索(Depth First Search)思想:假设初始状态是图中所有顶点均未被访问,则从某个顶点 v 出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和 v 有路径相通的顶点都被访问到。 若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
深度优先搜索是一个递归的过程

无向图的深度优先搜索

12.jpg
对上面的图 G1 进行深度优先遍历,从顶点 A 开始:

  1. 访问 A
  2. 访问 A 的邻接点 C (在第1步访问 A 之后,接下来应该访问的是 A 的邻接点,即 “C,D,F” 中的一个。但在本文的实现中,顶点 ABCDEFG 是按照顺序存储,C 在 “D和F” 的前面,因此,先访问 C)
  3. 访问 C 的邻接点 B
  4. 访问 C 的邻接点 D (在第3步访问了 C 的邻接点 B 之后,B 没有未被访问的邻接点;因此,返回到访问 C 的另一个邻接点 D)
  5. 访问 A 的邻接点 F
  6. 访问( F 的邻接点) G
  7. 访问 G 的邻接点 E

因此访问顺序是:A -> C -> B -> D -> F -> G -> E

有向图的深度优先搜索

13.png
对上面的图 G2 进行深度优先遍历,从顶点 A 开始:

  1. 访问 A
  2. 访问 B (在访问了 A 之后,接下来应该访问的是 A 的出边的另一个顶点,即顶点 B)
  3. 访问 C
  4. 访问 E (接下来访问 C 的出边的另一个顶点,即顶点 E)
  5. 访问 D
  6. 访问 F (接下应该回溯”访问 A 的出边的另一个顶点 F”)
  7. 访问 G

因此访问顺序是:A -> B -> C -> E -> D -> F -> G

图的广度优先搜索

广度优先搜索算法(Breadth First Search),又称为”宽度优先搜索”或”横向优先搜索”,简称 BFS
思想:从图中某顶点v出发,在访问了 v 之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止

无向图的广度优先搜索

14.jpg
对上图进行广度优先遍历,从顶点 A 开始:

  1. 访问 A
  2. 依次访问 C,D,F
  3. 依次访问 B,G (在第2步访问完 C,D,F 之后,再依次访问它们的邻接点。首先访问 C 的邻接点 B,再访问 F 的邻接点 G)
  4. 访问 E

因此访问顺序是:A -> C -> D -> F -> B -> G -> E

有向图的广度优先搜索

15.jpg
对上图进行广度优先遍历,从顶点 A 开始:

  1. 访问 A
  2. 访问 B
  3. 依次访问 C,E,F ( 在访问了 B 之后,接下来访问B的出边的另一个顶点,即 C,E,F)
  4. 依次访问 D,G

因此访问顺序是:A -> B -> C -> E -> F -> D -> G