数据结构(更新至链表,持续更新...)

133 阅读8分钟

继网络协议之大集合完结之后,我觉得很有必要再来一次数据结构的大集合。对于个人来说可以增强理解,如有问题,欢迎指正,多谢。话不多说,现在开始

数据结构

1- 数据结构概述

  • 数据结构是计算机的存储、组织方式。

  • 数据结构分为:

    • 线性结构

      • 线性表、数组、链表、栈、队列、哈希表等
    • 树形结构

      • 二叉树、AVL树、红黑树、堆、Trie、哈夫曼树、并查集
    • 图形结构

      • 邻接矩阵
      • 邻接表

2- 线性结构数组

2-1 线性表

  • 线性表是具 n 个相同类型元素的有限序列(n > = 0)。

  • 线性表中除了第一个元素(首节点)之外 ,其他任何元素都有直接前驱,除了最后一个元素(尾节点)外,其他任何元素都有直接后继。

  • 常见的线性表

    • 数组
    • 链表
    • 队列
    • 哈希表

2-2 数组

  • 数组存在一个致命的缺点无法动态修改容量

  • 数组是一种顺序存储的线性表,所有元素的内存地址是连续的就是顺序存储

  • 每一个int类型的数据占4个字节,所以 int[] arrays = new int[]{11,32,19} 在计算机中的存储图为:

    int[] arrays = new int[]{11,32,19}
    

image-20230109112726075.png

2-3 动态数组代码实现

  • 使用 java 实现动态数组

    package com.zh.线性表.动态数组;
    ​
    /**
     * @Author superMonkey
     * @Date 2022/9/22 15:02
     * @Description TODO
     */
    public class ArrayList<T> {
    ​
    ​
        private int size;
    ​
        private  T[] dynamicArrays;
    ​
        private static final int DEFAULT_LENGTH = 10;
    ​
    ​
        public ArrayList() {
            this(0);
        }
    ​
        public ArrayList(int len) {
            //创建数组时
            if (len <= 0){
                dynamicArrays = (T[]) new Object[DEFAULT_LENGTH];
            }else {
                dynamicArrays = (T[]) new Object[len];
            }
    ​
        }
    ​
        /**
         * 获取元素数量
         * size控制了使用者能够访问的下标范围是 [0 , size) , 所以元素访问直接返回size
         * @return
         */
        public int size(){
            return size;
        }
    ​
    ​
        /**
         * 是否为空
         * 如果size == 0 说明可以访问数组范围是 [0 , 0) , 这样的表示说明不能访问任何元素, 所以当前数组为空。
         */
        public boolean isEmpty(){
            return size == 0;
        }
    ​
    ​
        /**
         * 在指定位置添加元素
         *
         * @param element
         * @return
         */
        public int add(int index, T element) {
            //首先判断index的访问是不是在 可以访问的访问之内
            //正常来说可以访问的访问应该是 [0 , size) , 但是这里的判断是可以允许index == size , 因为这样就可以兼容在数据末尾添加元素
            if (index < 0 || index > size) {
                throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
            }
    ​
            //扩容的方法 , 因为你添加了一个元素,所以要保证动态数组能够容下
            //扩容的方法回判读数组加上一个元素之后是否能够存下,如果能能够存下,就不扩容了
            expandCapacity(size + 1);
    ​
            //要想在index上增加一个元素就必须将 index 已经index之后的所有元素向后移动,然后在把元素放到index上
            //我们从数组的最后一个元素开始往后移动 , 如果是在数组末尾添加这个元素,不进入for循环,所以末尾添加元素效率高
            for (int i = size - 1; i >= index; i--) {
                //实现了元素后移一位
                dynamicArrays[i + 1] = dynamicArrays[i];
            }
    ​
            //再让腾出位置来的index上的元素等于我们想要添加的值
            dynamicArrays[index] = element;
            size++;
            return size;
        }
    ​
    ​
        /**
         * 在末尾添加元素
         *
         * @param element
         * @return
         */
        public int add(T element) {
            add(size, element);
            return size;
        }
    ​
    ​
        /**
         * 扩容
         * @param length
         */
        public void expandCapacity(int length){
            //length是新数组的长度,如果长度小于已存在的动态数组的长度,直接结束,不需要扩容,因为动态数组长度是够的
            if (length <= dynamicArrays.length) return;
    ​
            int oldLength = dynamicArrays.length;
            //将数组扩大,将原来的值左移2位,就是扩到到原来的两倍,jdk11是 >> 1
            int newLength = oldLength + (oldLength >> 2);
            //将老数组的值全部赋值给新数组
            Object[] newArrays = new Object[newLength];
            for (int i = 0; i < size; i++) {
                newArrays[i] = dynamicArrays[i];
            }
            //修改私有元素动态数组的引用指向,指向新数组
            dynamicArrays = (T[]) newArrays;
    ​
            System.out.println(oldLength + "扩容到" + dynamicArrays.length);
        }
    ​
        /**
         * 删除元素
         * @param index
         * @return
         */
        public Object delete(int index) {
            //判读需要删除的位置是否合理
            if (index < 0 || index >= size){
                throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
            }
            //需要将要删除的位置,也就是index上的元素变成 index + 1 上的元素,依次类推
            Object old = dynamicArrays[index];
            for (int i = index + 1; i < size; i++) {
                dynamicArrays[i - 1] = dynamicArrays[i];
            }
            //将数组最后一个位置上的元素设置为空,同时 --size 将size减去了1,那么允许访问的范围就减小了1
            dynamicArrays[--size] = null;
            return old;
        }
    ​
        /**
         * 清空所有元素
         *
         * @return
         */
        public void clear() {
            //将动态数组所有可以访问的位置上的元素设置成null
            for (int i = 0; i < size; i++) {
                dynamicArrays[size] = null;
            }
            // seize = 0 ,那么动态数组就相当于没有任何位置是可以访问的
            size = 0;
        }
    ​
        /**
         * 修改元素
         *
         * @param index
         * @param element
         * @return
         */
        public Object set(int index, Object element) {
            if (index < 0 || index >= size){
                throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
            }
            Object old = dynamicArrays[index];
            dynamicArrays[index] = (T) element;
            return old;
        }
    ​
        /**
         * 获取元素
         *
         * @param index
         * @return
         */
        public Object get(int index) {
            if (index < 0 || index >= size){
                throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
            }
            return dynamicArrays[index];
        }
    ​
        /**
         * 查看元素位置
         * @return
         */
        public int indexOf(Object element){
            for (int i = 0; i < size; i++) {
                if (element.equals(dynamicArrays[i])){
                    return i;
                }
            }
            return -1;
        }
    ​
        /**
         * 是否包含某个元素
         * 调用查看元素位置,返回 -1 说明不存在该元素
         */
        public boolean contains(T element){
            return indexOf(element) != -1;
        }
    ​
        @Override
        public String toString() {
            StringBuilder stringBuilder = new StringBuilder();
    ​
            stringBuilder.append("size:").append(size).append(" [");
    ​
            for (int i = 0; i < size; i++) {
                if (i != 0){
                    stringBuilder.append(", ");
                }
                stringBuilder.append(dynamicArrays[i]);
            }
            stringBuilder.append("]");
    ​
            return stringBuilder.toString();
        }
    ​
    }
    ​
    

动态数组中dynamicArrays是一个在创建时就固定的数组,长度并不会改变。而是使用了size控制访问的范围。在dynamicArrays的范围上,移动size来当作数组的末尾值,这样的就实现了动态数组。

3- 线性结构链表(Linked List)

  • 动态数组存在一个明显缺点,扩容后,造成内存空间浪费。
  • 想要实现用多少内存就申请多少空间,链表就可以办到。

3-1 链表概述

  • 链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的。

  • 链表存储元素:

    • 链表存储元素是在节点对象(Node)中存储元素,这个节点 对象不止存储本身的元素,还存储了下一个节点对象的内存地址。

image-20230109113913681.png

3-2 使用java程序实现链表

  • 链表中,头节点的内存地址没有被指向,尾节点中指向下一个节点的内存地址指向null。

  • 链表设计思路

    1、链表里面因该有两个元素,一个是size(节点的个数),一个是first(指向第一个节点的地址),在节点里面应该也有两个元素,一个是element(表示元素内容),还有一个就是next(指向下一个元素的内存地址),需要使用到一个节点时,再去分配这个节点的内存空间。并且这个节点对象建议放做成内部类

    package com.zh.线性表.动态数组;
    ​
    /**
     * @Author superMonkey
     * @Date 2023/1/7 23:34
     * @Description TODO
     */
    public class LinkList<T> {
    ​
        private int size;
    ​
        private Node first;
    ​
        private static class Node<T>{
            T element;
            Node next;
        }
        
        private static class Node<T>{
            T element;
            Node next;
            public Node(T element , Node next){
                this.element = element;
                this.next = next;
            }
        }
    ​
    }
    ​
    

    2.链表的方法大部分和动态数组一样,所以需要提取一个公共类,但是链表和动态数组大部分方法实现内容都不一样,所以公共类就不合适,需要一个接口来标识。

    package com.zh.线性表.动态数组;
    ​
    /**
     * @Author superMonkey
     * @Date 2023/1/8 0:08
     * @Description TODO
     */
    public interface List<T> {
    ​
        int size(); // 元素的数量
    ​
        boolean isEmpty(); // 是否为空
    ​
        boolean contains(T element); // 是否包含某个元素
    ​
        int add(T element); // 添加元素到最后面
    ​
        T get(int index); // 返回index位置对应的元素
    ​
        T set(int index, T element); // 设置index位置的元素
    ​
        int add(int index, T element); // 往index位置添加元素
    ​
        T remove(int index); // 删除index位置对应的元素
    ​
        int indexOf(T element); // 查看元素的位置
    ​
        void clear(); // 清除所有元素
    }
    ​
    
    1. 但其实ArrayList和LinkLisk还是有很多方法和参数是一样的,所以可以用一个抽象类在List接口和ArrayList,LinkLisk之间隔一下,实现公共的一些方法。
    package com.zh.线性表.动态数组;
    ​
    /**
     * @Author superMonkey
     * @Date 2023/1/8 0:15
     * @Description TODO
     */
    public abstract class AbstractList<T> implements List<T>{
        protected int size;
    ​
        /**
         * 获取元素数量
         * size控制了使用者能够访问的下标范围是 [0 , size) , 所以元素访问直接返回size
         * @return
         */
        public int size(){
            return size;
        }
    ​
        /**
         * 是否为空
         */
        public boolean isEmpty(){
            return size == 0;
        }
    ​
    ​
        /**
         * 在末尾添加元素
         *
         * @param element
         * @return
         */
    ​
        public int add(T element) {
            add(size, element);
            return size;
        }
    ​
        /**
         * 是否包含某个元素
         * 调用查看元素位置,返回 -1 说明不存在该元素
         */
        public boolean contains(T element){
            return indexOf(element) != -1;
        }
    ​
    }
    ​
    
    1. LinkList具体实现

image-20230109114538918.png

```java
 package com.zh.线性表.动态数组;
​
/**
 * @Author superMonkey
 * @Date 2023/1/7 23:34
 * @Description TODO
 */
public class LinkList<T> extends AbstractList<T>{
​
    private int size;
​
    //这个first才是第一个节点,我们在第一个节点之前有一个LinkLisk
    private Node first;
​
    private static class Node<T>{
        T element;
        Node next;
        public Node(T element , Node next){
            this.element = element;
            this.next = next;
        }
    }
​
    /**
     * 清空元素
     * 清空元素只要两步,size = 0 ; first = null
     *  first 指向第一个节点对象,把 first 指向 null , 第一个节点对象就会销毁,第一个节点对象里的next本来是指向第二个节点对象的内存地址
     *  但是第一个节点对象被销毁了,所以第一个节点的next就不存在了,那么第二个节点对象就回被销毁,同理,清空了所有的元素。
     */
    @Override
    public void clear() {
        size = 0;
        first = null;
    }
​
    /**
     * node方法用于获取index位置的节点
     * 此方法仅仅只是在链表内部使用,不对外公开
     * @param index
     * @return
     */
    private Node<T> node(int index) {
        //检查下标是否合理 , 这段代码可以抽出来,变成一个单独检查下标的方法放到抽象类中,因为这个方法在ArrayList和LinkList是一样的
        if (index < 0 || index >= size){
            throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
        }
        //从第0个节点开始找,找到index,找到后返回index对应的节点
        Node<T> node = this.first;//获取第0个节点
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        return node;
    }
​
    @Override
    public T get(int index) {
        //调用 node 方法,所以不需要再判断下标是否合理
        return node(index).element;
    }
    
    @Override
    public T set(int index, T element) {
        Node<T> node = node(index);
        T old = node.element;
        node.element = element;
        return old;
    }
​
    /**
     * 在 index 处添加一个元素。先新建一个节点对象,将元素element放到新建里节点对象里,
     * 此时需要将 index 前面一个节点对象的 next 指向新的节点,将新的节点中next指向前一个节点对象原来的next指向的节点
     * @param index
     * @param element
     * @return
     */
    @Override
    public int add(int index, T element) {
        if (index < 0 || index > size){
            throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
        }
        if (index == 0) {
            //先将新节点的next指向原来的first,再将first指向新节点
            first = new Node(element, first);
        }else {
            //获取index前一个节点
            Node<T> previous = node(index - 1);
            //创建一个新节点,将前一个节点的next交给新节点的next,并将前一个节点的next指向新节点。
            previous.next = new Node<>(element , previous.next);
        }
        //链表长度 + 1
        size++;
        return size;
    }
​
​
    /**
     * 获得 index - 1 的node , 将index - 1 的node中的next 指向 index 对应node的next
     * @param index
     * @return
     */
    @Override
    public T remove(int index) {
       if (index < 0 || index > size){
            throw new IndexOutOfBoundsException("size:" + size + " , " + "index:" + index);
        }
        Node<T> node = first;
        if (index == 0){
            first = first.next;
        }else {
            Node<T> previous = node(index - 1);
            node = previous.next;//被删除的节点
            previous.next = node.next;
        }
​
        size--;
        return node.element;
    }
​
    @Override
    public int indexOf(T element) {
        //element是范性,要用equals比较,所以这里需要考虑null的情况。
        if (element == null) {
            Node<T> node = first;
            for (int i = 0; i < size; i++) {
                if (node.element == element) return i;
                node = node.next;
            }
        }else {
            Node<T> node = first;
            for (int i = 0; i < size; i++) {
                //node.element有可能是null,但这里element肯定不是空
                if (element.equals(node.element)) return i;
                node = node.next;
            }
        }
        return -1;
    }
​
    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
​
        stringBuilder.append("size:").append(size).append(" [");
​
        Node node = first;
        for (int i = 0; i < size; i++) {
            if (i != 0){
                stringBuilder.append(", ");
            }
​
            stringBuilder.append(node.element);
            node = node.next;
        }
        stringBuilder.append("]");
​
        return stringBuilder.toString();
    }
}
​
```