JAVA数据结构和算法:第二章(表)

线性表的定义

线性表,从名字上来看,像线连起来的表。每个元素都是连起来的,比如在体育课按照老师定好的队列排队的时候,有一个打头,一个收尾,中间的每个人都知道前面是谁,后面是谁,就像一根线将他们联系在一起,就可以称之为线性表。

这时候我们来看几个关键点,首先元素之间是有顺序的,并且第一个元素无前驱,最后一个元素无后继,其他的元素都有且只有一个前驱和后继,这样才能形成线性表。

我们用数学语言来进行定义,若有线性表(a1,a2,….,ai-1,ai,ai+1,…,an),则称表中ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素,a1没有直接前驱元素,an没有直接后继元素。线性表元素的个数定义为线性表的长度,当n=0时的特殊表我们称之为空表。

我们来举几个例子,每年的星座列表是不是线性表?当然是,因为星座以白羊打头,双鱼收尾,其中的都有且只有一个前驱和后继,所以完全符合线性表的条件。
那么公司的上下级关系是不是线性表?当然不是,因为一个上级会有很多个下级,每个人不只有一个后继和前驱,不符合线性表的条件。

前面我们明确定义了线性表的概念,与这些概念相关的是线性表应该具有的操作集合。

例如,向表中插入一个数据,或者获取表中的某个元素,删除某个元素,获取某个元素的直接前驱后继等操作,当然对于不同的应用,线性表应该有什么操作都是不同的,完全由程序设计者来决定,当然像增删查等必要的操作基本是每个线性表都需要具有的。

顺序存储结构

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

这里写图片描述

说白了,就是在内存中找一块位置,把一定的内存空间占了,然后把相同数据类型的数据元素按顺序存放在这儿。说到这儿,我们可能有人会想起来,和数组怎么那么像呢?对,线性表的每个数据元素类型都相同,所以我们完全可以通过数组来实现线性表的顺序存储结构

如果我们用数组来实现的话,我们就必须需要两个属性:线性表的长度,需要的数组长度。并且要保证线性表的长度要小于数组的长度,因为我们可能还需要执行插入操作。

java顺序结构线性表的实现

public class LinearList<T>  {
    /**
     * 对象数组,用数组来存放线性表的数据
     */
    protected Object[] element;

    /**
     * 线性表长度,记载元素个数
     */
    protected int len;

    /**
     * 默认构造方法,创建默认容量的空表
     */
    public LinearList() {
        this(64);
    }

    /**
     * 构造方法,创建容量为size的空表
     * 
     * @param size
     */
    public LinearList(int size) {
        this.element = new Object[size];
        this.len = 0;
    }

    /**
     * 判断顺序表是否空,若空返回true,O(1)
     */
    public boolean isEmpty() {
        return this.len == 0;
    }

    /**
     * 返回顺序表长度,O(1)
     */
    public int length() {
        return this.len;
    }

    /**
     * 返回第i(≥0)个元素。若i<0或大于表长则返回null,O(1)
     */

    public T get(int i) {
        if (i >= 0 && i < this.len) {
            return (T) this.element[i];
        }
        return null;
    }

    /**
     * 设置第i(≥0)个元素值为x。若i<0或大于表长则抛出序号越界异常;若x==null,不操作
     */
    public void set(int i, T x) {
        if (x == null)
            return;

        if (i >= 0 && i < this.len) {
            this.element[i] = x;
        } else {
            throw new IndexOutOfBoundsException(i + ""); // 抛出序号越界异常
        }
    }

    /**
     * 顺序表的插入操作 插入第i(≥0)个元素值为x。若x==null,不插入。 若i<0,插入x作为第0个元素;若i大于表长,插入x作为最后一个元素 
     * 思路:从最后一个元素往前遍历到第i个位置,分别将他们都往后移一位
     */
    public void insert(int i, T x) {
        if (x == null)
            return;

        // 若数组满了,则扩充数组容量
        if (this.len == element.length) {
            // temp也引用elements数组
            Object[] temp = this.element;
            // 重新申请一个双倍的数组
            this.element = new Object[temp.length * 2];
            // 复制数组元素,O(n)
            for (int j = 0; j < temp.length; j++) {
                this.element[j] = temp[j];
            }
        }

        // 下标容错
        if (i < 0)
            i = 0;

        if (i > this.len)
            i = this.len;

        // 元素后移,平均移动len/2
        for (int j = this.len - 1; j >= i; j--) {
            this.element[j + 1] = this.element[j];
        }
        this.element[i] = x;
        this.len++;
    }

    /**
     * 在顺序表最后插入x元素
     */
    public void append(T x) {
        insert(this.len, x);
    }

    /**
     * 顺序表的删除操作 删除第i(≥0)个元素,返回被删除对象。若i<0或i大于表长,不删除,返回null。
     * 思路:从删除元素的位置到最后遍历,每一个元素向前移一位
     */

    public T remove(int i) {
        if (this.len == 0 || i < 0 || i >= this.len) {
            return null;
        }

        T old = (T) this.element[i];
        // 元素前移,平均移动len/2
        for (int j = i; j < this.len - 1; j++) {
            this.element[j] = this.element[j + 1];
        }
        this.element[this.len - 1] = null;
        this.len--;
        return old;
    }

    /**
     * 删除线性表所有元素
     */
    public void removeAll() {
        this.len = 0;
    }

    /**
     * 查找,返回首次出现的关键字为key元素
     */
    @SuppressWarnings("unchecked")
    public T search(T key) {
        int find = this.indexOf(key);
        return find == -1 ? null : (T) this.element[find];
    }

    /**
     * 顺序表比较相等 比较两个顺序表是否相等 ,覆盖Object类的equals(obj)方法,O(n)
     */

    public boolean equals(Object obj) {
        if (this == obj)
            return true;

        if (obj instanceof LinearList) {
            LinearList<T> list = (LinearList<T>) obj;
            // 比较实际长度的元素,而非数组容量
            for (int i = 0; i < this.length(); i++) {
                if (!this.get(i).equals(list.get(i))) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }


    // 返回顺序表所有元素的描述字符串,形式为“(,)”,覆盖Object类的toString()方法
    public String toString() {
        String str = "(";
        if (this.len > 0)
            str += this.element[0].toString();
        for (int i = 1; i < this.len; i++)
            str += ", " + this.element[i].toString();
        return str + ") "; // 空表返回()
    }
}

上面是我们实现的顺序结构存储的线性表,我们已经发现了插入和删除是最麻烦的,步骤最多,需要很多遍历,我们来分析一下这两个操作的时间复杂度。
如果要插入的或者删除的是最后一个元素,那么不需要移动任何元素,这时候为O(1).
如果要插入的或者删除的是第一个元素,那么所有元素都需要移动,这时候为O(n).
而当执行存储和查询的时候,时间复杂度都为O(1),这也间接的说明了一个问题,它比较适合元素个数不变化的存取操作,而插入和删除很不方便

线性表顺序结构存储的优缺点

  • 优点:可以快速的存取表中任意位置的数据
  • 缺点:插入和删除需要移动元素,花费大量时间,并且当数组扩容时,难以确定存储空间的容量,容易造成存储空间的碎片化。
  • -

链式存储结构

前面我们已经证明了,线性表的顺序存储结构有一个很大的缺点,就是插入和删除操作时需要消耗大量时间,能不能想个办法解决一下呢?

首先我们需要思考一下为什么会造成这个情况,仔细思考其实是因为两个相邻元素之间在内存中也是紧挨着的,如果要插入自然要向后移,否则会出现覆盖,要删除,后面的元素自然也要向前移。那么我们应该怎么解决这一情况呢?我们可不可以让逻辑上相邻的两个元素,在内存中其实不是相邻的,而只需前一个元素知道后一个元素的地址,然后连起来就行了?对,这就是我们要讲的链式存储结构。

这里写图片描述

通常讲采用链式存储结构的线性表称为线性链表,从链接方式来看,链表可分为单链表、循环链表和双链表。从实现角度来看,链表分为动态链表和静态链表。

链表用一组任意的存储单元来存储线性表的元素,这组存储单元可以连续也可以不连续,所以现在每个元素中除了要存储数据,还要存储其后继元素的存储地址。我们把存储数据元素信息的域称为数据域,把存储后继元素位置的域称为指针域,这两部分共同组成一个数据元素单元,我们把它叫做结点(Node)。

单链表

每个结点中的指针域只包含下一个结点的位置,叫做单链表。单链表的最后一个结点指向为null。

这里写图片描述

单链表的java实现


首先,我们要实现一个结点类Node 

public class Node<T> {
    /**
     * 数据域,保存数据元素
     */
    public T data;
    /**
     * 地址域,引用后继结点
     */
    public Node<T> next;
    /**
     * 构造节点
     */
    public Node() {
        this(null, null);
    }

    /**
     * 构造结点,data指定数据元素,next指定后继结点
     * 
     * @param data
     * @param next
     */
    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    /**
     * 返回结点元素值对应的字符串
     */

    public String toString() {
        return this.data.toString();
    }

}
public class SingleLinkedList<T> {
        /**
         * 指定头结点
         */
        public Node<T> head;

        /**
         * 默认构造方法,构造空单链表
         */
        public SingleLinkedList() {
            // 创建头结点,data和next值均为null
            this.head = new Node<T>();
        }

        /**
         * 由指定数组中的多个对象构造单链表。采用尾插入构造单链表
         * 头插入就是每次新的结点在前面,例如第一个结点在头结点的前面,以此类推
         * 头插入不太符合我们逻辑,所以尾插入,生成的结点在后面
         * 
         * 若element==null,Java将抛出空对象异常;若element.length==0,构造空链表
         * 思路:让一个结点指向最后一个结点,刚开始就一个头结点,然后循环遍历数组,将最后一个结点的地址域设置为数组的元素
         * 
         * @param element
         */
        public SingleLinkedList(T[] element) {
            // 创建空单链表,只有头结点
            this();
            // last指向单链表最后一个结点
            Node<T> last = this.head;
            for (int i = 0; i < element.length; i++) {
                last.next = new Node<T>(element[i], null);
                last = last.next;
            }
        }

        /**
         * 判断单链表是否空,O(1)
         * 判断头节点的地址域是否为空即可
         */

        public boolean isEmpty() {
            return this.head.next == null;
        }

        /**
         * 返回单链表长度,O(n), 基于单链表遍历算法
         * 数组中长度定义时就确定,单链表就必须得从头开始向后计数
         */

        public int length() {
            //定义i用来计数
            int i = 0;
            // p从单链表第一个结点开始
            Node<T> p = this.head.next;
            // 若单链表未结束
            while (p != null) {
                i++;
                // p到达后继结点
                p = p.next;
            }
            return i;
        }

        /**
         * 获得第i(≥0)个元素,若i<0或大于表长则返回null,O(n)
         */

        public T get(int i) {
            if (i >= 0) {
                //定义一个头结点,从头开始查找
                Node<T> p = this.head.next;
                //如果j在跳出循环时不为空,说明i处元素存在,否则说明元素不存在
                for (int j = 0; p != null && j < i; j++) {
                    p = p.next;
                }

                // p指向第i个结点
                if (p != null) {
                    return p.data;
                }
            }
            return null;
        }

        /**
         * 设置第i(≥0)个元素值为x。若i<0或大于表长则抛出序号越界异常;若x==null,不操作。O(n)
         */

        public void set(int i, T x) {
            if (x == null)
                return;
            //还是从头结点开始
            Node<T> p = this.head.next;
            for (int j = 0; p != null && j < i; j++) {
                p = p.next;
            }

            //如果插入的位置>0,并且循环后的p不为null,则赋值给这个结点
            if (i >= 0 && p != null) {
                p.data = x;
            } else {
                throw new IndexOutOfBoundsException(i + "");
            }
        }

        /**
         * 插入第i(≥0)个元素值为x。若x==null,不插入。 若i<0,插入x作为第0个元素;若i大于表长,插入x作为最后一个元素。O(n)
         * 初始化从第一个结点开始,当j<i时遍历节点,若到末尾结点为空,说明第i个元素不存在
         * 如果存在,就创建一个新结点,然后相互赋值即可
         */

        public void insert(int i, T x) {
            // 不能插入空对象
            if (x == null) {
                return;
            }

            // p指向头结点
            Node<T> p = this.head;
            // 寻找插入位置
            for (int j = 0; p.next != null && j < i; j++) {
                // 循环停止时,p指向第i-1结点或最后一个结点
                p = p.next;
            }
            // 插入x作为p结点的后继结点,如果i<0,则p还是为头结点,所以是在最前面插入
            //如果i>链表长度,则p指向最后一个元素,所以是插入到最后
            p.next = new Node<T>(x, p.next);
        }

        /**
         * 在单链表最后添加x对象,O(n)
         * 直接定义一个比较大的数,然后插入的时候会遍历,当结点为空时说明是最后,然后跳出循环
         * 直接插入到最后
         */

        public void append(T x) {
            insert(Integer.MAX_VALUE, x);
        }

        /**
         * 删除第i(≥0)个元素,返回被删除对象。若i<0或i大于表长,不删除,返回null。O(n)
         * 思路:从头结点开始遍历,如果遍历结束,结点为空,则说明第i个元素不存在,否则就查找成功
         * 然后将删除元素的地址域赋值给前一个元素即可,然后返回删除元素的数据
         */

        public T remove(int i) {
            if (i >= 0) {
                Node<T> p = this.head;
                for (int j = 0; p.next != null && j < i; j++) {
                    //因为我们是从链表元素是从0开始,所以p并不是我们要删除的元素,p的下一个才是
                    p = p.next;
                }

                if (p != null) {
                    // 获得原对象
                    T old = p.next.data;
                    // 删除p的后继结点
                    p.next = p.next.next;
                    return old;
                }
            }
            return null;
        }

        /**
         * 删除单链表所有元素 Java将自动收回各结点所占用的内存空间 
         * 我们只需要将头结点设为null,其他的jvm会帮我们回收垃圾
         */

        public void removeAll() {
            this.head.next = null;
        }

        /**
         * 顺序查找关键字为key元素,返回首次出现的元素,若查找不成功返回null
         * key可以只包含关键字数据项,由T类的equals()方法提供比较对象相等的依据
         */

        public T search(T key) {
            if (key == null)
                return null;
            for (Node<T> p = this.head.next; p != null; p = p.next){
                    if (p.data.equals(key))
                        return p.data;
                   } 
            return null;
        }

        /**
         * 返回单链表所有元素的描述字符串,形式为“(,)”,覆盖Object类的toString()方法,O(n)
         */

        public String toString() {
            String str = "(";
            for (Node<T> p = this.head.next; p != null; p = p.next) {
                str += p.data.toString();
                if (p.next != null)
                    str += ","; // 不是最后一个结点时后加分隔符
            }
            return str + ")"; // 空表返回()
        }

        /**
         * 比较两条单链表是否相等
         */

        public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (obj instanceof SingleLinkedList) {
                SingleLinkedList<T> list = (SingleLinkedList<T>) obj;
                return equals(this.head.next, list.head.next);
            }
            return false;
        }

        /**
         * 比较两条单链表是否相等,递归方法
         * 
         * @param p
         * @param q
         * @return
         */
        private boolean equals(Node<T> p, Node<T> q) {
            return p == null && q == null || p != null && q != null
                    && p.data.equals(q.data) && equals(p.next, q.next);
        }
}

单链表的优缺点

通过代码的编写,我们发现单链表的查找方法时间复杂度为O(n),而插入和删除为O(1)

  • 优点:执行插入和删除效率高,不需要事先分配空间,元素个数不受限制,不会浪费空间

  • 缺点:查找效率比较低

我们可以得出一些结论,若线性表需要频繁查找,很少进行插入和删除操作时,则使用顺序存储结构。反之,需要大量的插入和删除时,则使用链式存储结构。

循环链表

循环链表是一个首尾相连的链表,将单链表最后一个结点的地址域改为指向表头结点,就得到了单链循环链表,称为循环单链表。

循环单链表和单链表的主要差异就在循环的判断条件上,单链表判断循环结束是看地址域是否为空,而循环单链表则是判断地址域是否是头结点。在前面的单链表中,我们总是用一头结点来指向我们的第一个数据元素,这样非常方便。在单链表中,我们有了头结点,可以很快的访问到第一个元素,访问最后一个元素却需要O(n),循环链表就可以解决这个问题,使访问第一个元素和最后一个元素都是O(1),怎么实现呢?再添加一个指向尾部的结点。

循环单链表的实现


public class CircleSingleLinkedList<T> {
        /**
         * 头指针,指向循环单链表的头结点
         * 尾指针,指向循环单链表的最后一个结点
         */
        public Node<T> head;
        public Node<T> tail;

        /**
         * 默认构造方法,构造空循环单链表
         * 即让自己指向自己
         * 然后让尾指针指向最后一个结点,此时也是指向头结点
         */
        public CircleSingleLinkedList() { 
            this.head = new Node<T>();
            this.head.next = this.head;
            this.tail=new Node<T>();
            this.tail.next=this.head;
        }

        /**
         * 判断循环单链表是否空
         * 判断头结点的地址域是不是自己即可
         */

        public boolean isEmpty() {
            return this.head.next == this.head;
        }

        /**
         * 返回循环单链表长度,单链表遍历算法,O(n)
         * 注意我们说的判断条件的区别
         */

        public int length() {
            int i = 0;
            for (Node<T> p = this.head.next; p != this.head; p = p.next) {
                i++;
            }
            return i;
        }

        /**
         * 返回第i(≥0)个元素,若i<0或大于表长则返回null,O(n)
         */

        public T get(int i) {
            if (i >= 0) {
                Node<T> p = this.head.next;
                for (int j = 0; p != this.head && j < i; j++)
                    p = p.next;
                if (p != this.head)
                    return p.data; // p指向第i个结点
            }
            return null;
        }

        /**
         * 设置第i(≥0)个元素值为x。若i<0或大于表长则抛出序号越界异常;若x==null,不操作。O(n)
         */

        public void set(int i, T x) {
            // 不能设置空对象
            if (x == null)
                return;
            Node<T> p = this.head.next;
            for (int j = 0; p != this.head && j < i; j++)
                p = p.next;
            // p指向第i个结点
            if (i >= 0 && p != this.head)
                p.data = x;
            else
                throw new IndexOutOfBoundsException(i + ""); // 抛出序号越界异常
        }


        public void insert(int i, T x) {
            // 不能插入空对象
            if (x == null) 
                return;

            // p指向头结点
            Node<T> p = this.head;
            // 寻找插入位置
            for (int j = 0; p.next != this.head && j < i; j++) {
                // 循环停止时,p指向第i-1结点或最后一个结点
                p = p.next;
            }
            // 插入x作为p结点的后继结点,包括头插入(i<=0)、中间/尾插入(i>0) 
            p.next = new Node<T>(x, p.next);
            //每插入一个都要保证尾结点指向最后一个元素
            this.tail.next=p.next;
        }


        public void append(T x) {
            insert(Integer.MAX_VALUE, x);
        }

        /**
         * 删除第i(≥0)个元素,返回被删除对象。若i<0或i大于表长,不删除,返回null。O(n)
         */

        public T remove(int i) {
            if (i >= 0) {
                Node<T> p = this.head;
                for (int j = 0; p.next != this.head && j < i; j++) {
                    p = p.next;
                }

                if (p != null) {
                    // 获得原对象
                    T old = p.next.data;
                    // 删除p的后继结点
                    p.next = p.next.next;
                    return old;
                }
            }
            return null;
        }

        /**
         * 删除单链表所有元素 Java将自动收回各结点所占用的内存空间
         */

        public void removeAll() {
            this.head.next = this.head;
        }

        /**
         * 顺序查找关键字为key元素,返回首次出现的元素,若查找不成功返回null
         * key可以只包含关键字数据项,由T类的equals()方法提供比较对象相等的依据
         */

        public T search(T key) {
            if (key == null)
                return null;
            for (Node<T> p = this.head.next; p != this.head; p = p.next)
                if (p.data.equals(key))
                    return p.data;
            return null;
        }

        /**
         * 返回循环单链表所有元素的描述字符串
         */

        public String toString() {
            String str = "(";
            Node<T> p = this.head.next;
            // 遍历单链表的循环条件改变了
            while (p != this.head) {
                str += p.data.toString();
                if (p != this.head)
                    str += ", "; // 不是最后一个结点时后加分隔符
                p = p.next;
            }
            return str + ")";
        }

        /**
         * 比较两条单链表是否相等
         */
        @SuppressWarnings("unchecked")

        public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (obj instanceof CircleSingleLinkedList) {
                CircleSingleLinkedList<T> list = (CircleSingleLinkedList<T>) obj;
                return equals(this.head.next, list.head.next);
            }
            return false;
        }

        /**
         * 比较两条单链表是否相等,递归方法
         * 
         * @param p
         * @param q
         * @return
         */
        private boolean equals(Node<T> p, Node<T> q) {
            return p == null && q == null || p != null && q != null
                    && p.data.equals(q.data) && equals(p.next, q.next);
        }  

        //合并两条循环单链表 
        /*有了尾结点这个工作将变得无比轻松,我们只需要让A链表的尾部指向B链表的第一个结点(不是头结点)
        然后让B链表的尾结点指向A链表的头结点即可
        */
        public void merge(CircleSingleLinkedList t) {
             Node <T> n=this.tail.next.next;
             this.tail.next.next=t.tail.next.next.next;
             t.tail.next.next=n;
         }
}

双向链表

我们在单链表中如果要查找上一个结点的话,那就得从头开始循环了,最坏的时间复杂度就是O(n),
你会想这也太麻烦了,我能不能像next直接查找下一个结点一样,设计链表可以直接查找上一个结点呢?当然可以,为了克服上述问题,前辈们设计了双向链表。

双向链表:就是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表中的每个结点都有两个指针域,一个指向直接后继,一个指向直接前驱。

既然单链表可以有循环链表,那么双向链表当然也可以循环。

双向循环链表为空时

这里写图片描述

双向循环链表不为空时

这里写图片描述

双向循环链表的实现

//先创建一个指向前驱和后继结点的结点对象

public class DLinkNode<T> {
     public T data;

        /**
         * pred指向前驱结点,next指向后继结点
         */
        public DLinkNode<T> pred;
        public DLinkNode<T> next;

        /**
         * 构造节点,data指定元素,pred指向前驱节点,next指向后继节点
         */
        public DLinkNode(T data, DLinkNode<T> pred, DLinkNode<T> next) {
            this.data = data;
            this.pred = pred;
            this.next = next;
        }
        /**
         * 默认构造器
         */
        public DLinkNode() {
            this(null, null, null);
        }
}
//实现双向循环链表

public class CircleDoubleLinkedList<T> { 
    //头结点
    public DLinkNode<T> head;

    //默认构造方法
    public CircleDoubleLinkedList() {
        this.head=new DLinkNode<T>();
        this.head.pred=this.head;
        this.head.next=this.head;
    } 

    //将数组构造成循环双链表
    public CircleDoubleLinkedList(T[] element) {
        this(); 
        //rear指向最后的结点
        DLinkNode<T> rear=this.head;
        for(int i=0;i<element.length;i++) {
            rear.next=new  DLinkNode<T>(element[i],rear,this.head);
            rear=rear.next;
        } 
        //将头结点的直接前驱结点设置为最后一个结点
        this.head.pred=rear;
    }


        public boolean isEmpty() {
            return this.head.next == this.head;
        }

        /**
         * 获取链表的长度
         */
        public int length() {
            int i = 0;
            for (DLinkNode<T> p = this.head.next; p != this.head; p = p.next) {
                i++;
            }
            return i;
        }

        /**
         * 返回第i(≥0)个元素,若i<0或大于表长则返回null,
         */

        public T get(int i) {
            if (i >= 0) {
                DLinkNode<T> p = this.head.next;
                for (int j = 0; p != this.head && j < i; j++) {
                    p = p.next;
                }

                // p指向第i个结点
                if (p != null) {
                    return p.data;
                }
            }
            return null;
        }

        /**
         * 设置第i(≥0)个元素值为x。若i<0或大于表长则抛出序号越界异常;若x==null,不操作。O(n)
         */

        public void set(int i, T x) {
            if (x == null)
                return;

            DLinkNode<T> p = this.head.next;
            for (int j = 0; p != this.head && j < i; j++) {
                p = p.next;
            }

            // p指向第i个结点
            if (i >= 0 && p != null) {
                p.data = x;
            } else {
                throw new IndexOutOfBoundsException(i + "");
            }
        }

        /**
         * 插入第i(≥0)个元素值为x。若x==null,不插入。 若i<0,插入x作为第0个元素;若i大于表长,插入x作为最后一个元素。O(n)
         */

        public void insert(int i, T x) {
            if (x == null)
                return;
            DLinkNode<T> p = this.head;
            // 寻找插入位置 循环停止时,p指向第i-1个结点
            for (int j = 0; p.next != this.head && j < i; j++) {
                p = p.next;
            }
            // 插入在p结点之后,包括头插入、中间插入
            DLinkNode<T> q = new DLinkNode<T>(x, p, p.next);
            p.next.pred = q;
            p.next = q;
        }

        /**
         * 在循环双链表最后添加结点,O(1)
         */
        public void append(T x) {
            if (x == null)
                return;
            // 插入在头结点之前,相当于尾插入
            DLinkNode<T> q = new DLinkNode<T>(x, head.pred, head);
            head.pred.next = q;
            head.pred = q;
        }

        /**
         * 删除第i(≥0)个元素,返回被删除对象。若i<0或i大于表长,不删除,返回null。O(n)
         */

        public T remove(int i) {
            if (i >= 0) {
                DLinkNode<T> p = this.head.next;
                // 定位到待删除结点
                for (int j = 0; p != this.head && j < i; j++) {
                    p = p.next;
                }

                if (p != head) {
                    // 获得原对象
                    T old = p.data;
                    // 删除p结点自己
                    p.pred.next = p.next;
                    p.next.pred = p.pred;
                    return old;
                }
            }
            return null;
        }

        /**
         * 删除循环双链表所有元素
         */

        public void removeAll() {
            this.head.pred = this.head;
            this.head.next = this.head;
        }

        /**
         * 顺序查找关键字为key元素,返回首次出现的元素,若查找不成功返回null
         * key可以只包含关键字数据项,由T类的equals()方法提供比较对象相等的依据
         */

        public T search(T key) {
            if (key == null)
                return null;
            for (DLinkNode<T> p = this.head.next; p != this.head; p = p.next)
                if (key.equals(p.data))
                    return p.data;
            return null;
        }

        /**
         * 返回循环双链表所有元素的描述字符串,循环双链表遍历算法,O(n)
         */

        public String toString() {
            String str = "(";
            for (DLinkNode<T> p = this.head.next; p != this.head; p = p.next) {
                str += p.data.toString();
                if (p.next != this.head)
                    str += ",";
            }
            return str + ")"; // 空表返回()
        }

        /**
         * 比较两条循环双链表是否相等,覆盖Object类的equals(obj)方法
         */
        @SuppressWarnings("unchecked")
        public boolean equals(Object obj) {
            if (obj == this)
                return true;
            if (!(obj instanceof CircleDoubleLinkedList))
                return false;
            DLinkNode<T> p = this.head.next;
            CircleDoubleLinkedList<T> list = (CircleDoubleLinkedList<T>) obj;
            DLinkNode<T> q = list.head.next;
            while (p != head && q != list.head && p.data.equals(q.data)) {
                p = p.next;
                q = q.next;
            }
            return p == head && q == list.head;
        }
}    

双向循环链表就是典型的用空间换时间。

JAVA API中的表实现

JAVA的类库中,包含着很多数据结构的实现,设计的很精妙,所以说JDK源码就是我们非常好的学习工具。Collection接口中有很多使用我们数据结构来实现的集合类,我们今天就来看看集合中的那些表实现。

Collecation接口中定义了很多集合类中应该有的方法,并且实现了Iterable接口,实现Iterable接口的类可以使用增强for循环。

实现Iterable接口的集合必须提供Iterator方法,该方法返回一个Iterator对象,Iterator也是java.util包中的一个接口,Iterator接口主要是为了遍历集合,将当前的位置在对象内部存储起来,然后就可以进行遍历。当一个增强for循环在循环一个实现Iterable接口的对象时,其实增强for循环内部就是使用的Iterator对象在进行遍历。

好了,说了这么多,我们扯得有点远,我们这次最大的目的是来看看jdk中的表实现,它由java.util包中的List接口指定,它下面有很多实现类,例如ArrayList类就是顺序结构线性表的实现,存取指定位置元素比较方便,而插入和删除效率就很低。LinkedList类则是双链表的实现,存取指定位置元素效率比较低,而插入和删除的效率则高于ArrayList。

尽管前面我们已经知道了顺序结构线性表和链表的差别,这里我们再来看一下ArrayList和LinkedList的差别。

  • 不论是对于ArrayList还是LinkedList而言,在后端添加N个元素的操作运行时间都是O(n).

  • 如果从集合前面添加N个元素的话,,LinkedList的运行时间是O(n),而ArrayList每添加一个就是O(n),添加n个则是O(n^2)

  • 对于通过get()计算集合中n个数的和的方法,ArrayList的运行时间是O(n),LinkedList则为O(n^2),但是如果使用迭代器的话,那么任意的List集合运行时间都是O(n),因为迭代器会记录当前
    位置。

小例子:remove方法对LinkedList的使用

我们来做一个例子,将一个表中的所有偶数都删除,我们当然不可能使用ArrayList,因为每次操作都是O(n),所以我们要使用LinkedList..

我们先来一种我们正常思考的算法。

    public void removeEven(LinkedList<Integer> lst) {
        int i=0;
        while(i<lst.size()) {
            if(lst.get(i)%2==0) {
                lst.remove(i);
            }
            i++;
        }
     }

上面是我们最经常想到的方法,但是也暴露出了两个问题,首先LinkedList的get的效率是很低的,为O(n),并且remove方法效率也很低,也是O(n)。所以整个方法的时间为O(n^2) 

怎么解决这两个问题呢?我们用我们前面提到的迭代器。

    public void removeEven(LinkedList<Integer> lst) {
        Iterator<Integer> i=lst.iterator();
        while(i.hasNext()) {
            if(i.next()%2==0) {
                i.remove();
            }       
        }
    }  
    使用迭代器的话,删除只需要花费常数时间,因为迭代器就在删除元素这里,所以整个函数只需要O(n)的时间,效率大大提高。

ArrayList的实现

我们前面实现的顺序结构存储的线性表其实就是ArrayList的内部实现,如果有什么模糊的地方可以去前面看看源代码,这里我们来实现点更接近ArrayList的功能,那就是迭代器。

我们先来编写一个迭代器

public class LinearListIterator<T> implements Iterator<T> {

    private int current=0;
    private LinearList<T> linearList;


    public LinearListIterator(LinearList<T> linearList) {
        this.linearList=linearList;
    } 
    //判断当前项有没有大于表的长度
    @Override
    public boolean hasNext() {
        return current<linearList.len;
    }
    //返回当前项,并加1记录当前位置
    @Override
    public T next() {
        return (T) linearList.element[current++];
    } 


}

我们自己编写的ArrayList必须实现Iterable接口,然后实现他的方法,返回一个迭代器对象

@Override
public Iterator<T> iterator() {
    return new LinearListIterator(this);
}
我们测试一下我们的迭代器 
    public static void main(String[] args) {

        LinearList<Integer> l=new LinearList<Integer>();
        l.append(1);
        l.append(2);
        l.append(3);
        l.append(4);
        l.append(5);
        LinearListIterator<Integer> iterator=(LinearListIterator<Integer>) l.iterator(); 
        //可以正常输出
        while(iterator.hasNext()) {
            System.out.println(iterator.next());
        }   
    } 

当然ArrayList内部采用的是内部类来实现迭代器,这样有一个很明显的优点就是可以直接操作当前的集合对象,而不用别人再来传递,并且可以获得当前集合的所有属性来操作。

LinkedList的实现
JDK中的LinkedList是一个双向链表,并且没有循环。我们前面也实现了,具体可以看看前面的代码。
至于LinkedList迭代器的实现,我想大家有了前面的基础,会很容易的实现。

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
第0章 Java程序设计基础 1 【习0.1】 实验0.1 哥德巴赫猜想。 1 【习0.2】 实验0.2 杨辉三角形。 1 【习0.3】 实验0.3 金额的中文大写形式。 1 【习0.4】 实验0.4 下标和相等的数字方阵。 1 【习0.5】 实验0.5 找出一个二维数组的鞍点 2 【习0.6】 实验0.6 复数类。 2 【习0.7】 实验0.8 图形接口与实现图形接口的类 2 第1章 绪论 3 【习1.1】 实验1.1 判断数组元素是否已按升序排序。 3 【习1.2】 实验1.3 用递归算法求两个整数的最大公因数。 3 第2章 线性表 5 【习2.1】 习2-5 图2.19的数据结构声明。 5 【习2.2】 习2-6 如果在遍历单链时,将p=p.next语句写成p.next=p,结果会怎样? 5 【习2.3】 实验2.2 由指定数组中的多个对象构造单链。 5 【习2.4】 实验2.2 单链的查找、包含、删除操作详见8.2.1。 5 【习2.5】 实验2.2 单链的替换操作。 6 【习2.6】 实验2.2 首尾相接地连接两条单链。 6 【习2.7】 实验2.2 复制单链。 6 【习2.8】 实验2.2 单链构造、复制、比较等操作的递归方法。 7 【习2.9】 建立按升序排序的单链(不带头结点)。 8 【习2.10】 实验2.6 带头结点的循环双链类,实现线性表接口。 10 【习2.11】 实验2.5 建立按升序排序的循环双链。 14 第3章 栈和队列 17 【习3.1】 习3-5 栈和队列有何异同? 17 【习3.2】 能否将栈声明为继承线性表,入栈方法是add(0,e),出栈方法是remove(0)?为什么? 17 【习3.3】 能否用一个线性表作为栈的成员变量,入栈方法是add(0,e),出栈方法是remove(0)?为什么? 17 【习3.4】 能否将队列声明为继承线性表,入队方法是add(e),出队方法是remove(0)?为什么? 17 第4章 串 18 【习4.1】 实验4.6 找出两个字符串中所有共同的字符。 18 【习4.2】 习4-9(1) 已知目标串为"abbaba"、模式串为"aba",画出其KMP算法的匹配过程,并给出比较次数。 18 【习4.3】 习4-9(2) 已知target="ababaab"、pattern="aab",求模式串的next数组,画出其KMP算法的匹配过程,并给出比较次数。 18 第5章 数组和广义 20 【习5.1】 求一个矩阵的转置矩阵。 20 第6章 树和二叉树 21 【习6.1】 画出3个结点的各种形态的树和二叉树。 21 【习6.2】 找出分别满足下面条件的所有二叉树。 21 【习6.3】 输出叶子结点。 21 【习6.4】 求一棵二叉树的叶子结点个数。 22 【习6.5】 判断两棵二叉树是否相等。 22 【习6.6】 复制一棵二叉树。 23 【习6.7】 二叉树的替换操作。 23 【习6.8】 后根次序遍历中序线索二叉树。 24 第7章 图 25 第8章 查找 26 【习8.1】 实验8.1 顺序的查找、删除、替换、比较操作。 26 【习8.2】 实验8.2 单链的全部替换操作。 28 【习8.3】 实验8.2 单链的全部删除操作。 28 【习8.4】 折半查找的递归算法。 29 【习8.5】 二叉排序树查找的递归算法。 29 【习8.6】 二叉排序树插入结点的非递归算法。 30 【习8.7】 判断一棵二叉树是否为二叉排序树。 31 第9章 排序 32 【习9.1】 判断一个数据序列是否为最小堆序列。 32 【习9.2】 归并两条排序的单链。 32 【习9.3】 说明二叉排序树与堆的差别。 34

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值