一起学数据结构:线性表

线性表

目录:

1.线性表抽象数据类型

线性表是其组成元素间具有线性关系的一种线性结构,对线性表的基本操作主要有获取元素值、遍历、插入、删除、查找、替代和排序等,插入和删除可以在线性表的任意位置进行。线性表可以采用顺序存储和链式存储结构表示。

线性表的特点:
1.有且仅有1个开始结点,没有直接前驱,有1个直接后继;
2.有且仅有1个结束结点,没有直接后继,有1个直接前驱;
3.其它结点均有1个直接前驱和1个直接后继。

线性表(linear list) 是由 n(n>=0) 个类型相同的元素组成的有限序列。
(其实这个接口就是List,为什么重写一遍呢而不是直接贴上jdk源码?是我在参加阿里的面试时,阿里的面试官问我:有没有自己去重写jdk的一些源码?重写这些源码去学习这些底层的实现方式)

创建一个线性表接口LList.java,信息如下:

public interface LList<T> {
    /**空判断方法*/
    boolean isEmpty();
    /**线性表长度*/
    int size();
    /**返回第i个元素*/
    T get(int index);
    /**设置第i个元素为t*/
    void set(int index,T t);
    /**插入t作为第i个元素*/
    void insert(int index,T t);
    /**在线性表最后插入t*/
    void add(T t);
    /**删除第i个元素*/
    T remove(int i);
    /**删除全部元素*/
    void removeAll();
    /**查找*/
    T search(T key);
}

由于线性表可以采用顺序存储和链式存储结构表示,在创建两个实现类:

//顺序存储
public class SequenceList<T> implements LList<T> 
//链式存储结构
public class SinglyLinkedList<T> implements LList<T> 

2.线性表的顺序表示和实现

2.1 线性表的顺序存储结构

线性表的顺序存储是用一组连续的内存单元依次存放线性表的数据元素,元素在内存的物理存储次序与它们在线性表中的逻辑相同,这种方式被称为顺序表(sequence list)

线性表的数据元素ai的存储地址是它在线性表中位置i的线性函数。如图所示:

线性表存储结构.PNG

顺序表通常采用数组存储数据元素。将线性表的数据元素顺序存放在数组中,数组元素在数组总的物理顺序与线性表中元素的顺序关系相同。
数组是顺序存储随机存取结构,占用一组连续的存储单元,通过下标(序号)识别元素,元素地址是下标的线性函数。一个下标能够唯一确定一个元素,存取任何一个元素所花费的时间是O(1)

2.2顺序表

类SequenceList<T>实现了接口LList<T>

public class SequentialList<T> implements LList<T> {
    
    private Object[] element;
    private int len;
    
    public SequentialList(int size){
        //如果size<0,会抛出负数组长度异常
        this.element = new Object[size];
        this.len = 0;
    }

    /**
     * 创建容器的默认长度
     */
    public SequentialList() {
        this(64);
    }

    //时间复杂度为O(1)
    @Override
    public boolean isEmpty() {
        return this.len == 0;
    }

    //时间复杂度为O(1)
    @Override
    public int size() {
        return this.len;
    }
    
    //时间复杂度为O(1)
    @Override
    public T get(int index) {
        if (index >= 0 && index < this.len){
            return (T) this.element[index];
        }
        return null;
    }

    //时间复杂度为O(1)
    @Override
    public void set(int index, T t) {
        if (t == null){
            return;
        }
        if (index >= 0 && index < this.len){
            this.element[index] = t;
        }else {
            throw new IndexOutOfBoundsException(index+"");
        }
    }
    
}

2.3 顺序表的插入与删除

顺序表的插入和删除操作要移动数据元素。

插入元素时如果数组已满,则不能插入,会报数据溢出异常(IndexOutOfBoundsException)。解决数据溢出的办法是,将这个数组扩容。
而jdk实现的方式是申请一个更大的数组并复制全部数组元素,这样就扩充了顺序表的容量。

public class SequentialList<T> implements LList<T> {
    //时间复杂度为O(n)
    public void insert(int index, T t) {
        if (t == null){
            return;
        }
        //如果数组满了,则扩容顺序表的容量
        if (this.len == element.length){
            Object[] tmp = this.element;
            this.element = new Object[tmp.length*2];
            for (int i = 0;i<tmp.length;i++){
                this.element[i] = tmp[i];
            }
        }

        //下标容错
        if (index < 0){
            index = 0;
        }
        if (index > this.len){
            index = this.len;
        }

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

        this.element[index] = t;
        this.len++;
    }

    public void add(T t) {
        insert(this.len,t);
    }
}
public class SequentialList<T> implements LList<T> {

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

        T old = (T) this.element[i];
        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;
    }
}

2.5 顺序表的浅拷贝与深拷贝

一个类的构造方法,如果其参数是该对象,则称为拷贝构造方法。如:

public Student(Student student){ …… }

拷贝构造方法的功能是复制对象,以形式参数的实例值初始化当前新创建对象。

  • 顺序表的浅拷贝:如果一个类将拷贝构造方法实现为逐域拷贝,即将当前对象的各种成员变量值赋值为实际参数对应各成员变量值,称为浅拷贝。
    如SequentialList类的浅拷贝构造方法:
public class SequentialList<T> implements LList<T> {
    /**
     * 浅拷贝的构造方法
     */
    public SequentialList(SequentialList<T> list){
        this.element = list.element;
        this.len = list.len;
    }
}

在Java中数据类型是基本类型时,浅拷贝能够实现对象复制功能,数组和类是引用类型,两个数组/对象之间的赋值是引用赋值,数组赋值过程中没有申请新的存储空间,
对象赋值过程中没有创建新的实例。因此当成员变量的数据类型是引用类型时,浅拷贝只复制了对象引用,并没有真正实现对象复制功能。

  • 顺序表的深拷贝:一个类包含引用类型的成员变量时,该类声明的拷贝构造函数,不仅复制对象的所有基本类型成员变量值,
    还重新申请引用类型变量占用的动态存储空间,并复制其中所有对象。
    如SequentialList类的深拷贝构造方法:
public class SequentialList<T> implements LList<T> {
    /**
     * 深拷贝的构造方法
     */
    public SequentialList(SequentialList<T> list){
            this.len = list.len;
            this.element = new Object[list.element.length];
            for (int i = 0;i<list.element.length;i++){
                this.element[i] = list.element[i];
            }
    }
}

结合顺序表的特点思考一下约瑟夫环的问题

有n个人,环成一圈开始报数,从s数起,数到m就枪毙一个,然后继续从s数起,数到d就枪毙一个,直到所有枪毙。输出所有人的死亡顺序。

public class Josephus {

    /**
     * @param number 总数量
     * @param start 开始位置
     * @param distance 执行位置
     */
    public Josephus(int number,int start,int distance){
        SequentialList<String> list = new SequentialList<>(number);
        for (int i= 0;i<number;i++){
            list.add((char)('A'+i)+"");
        }
        System.out.println("约瑟夫环("+number+","+start+","+distance+")");
        System.out.println(list.toString());

        int i = start;
        while (list.size() > 1){
            i = (i + distance-1)%list.size();
            System.out.println("删除"+list.remove(i).toString());
            System.out.println(list.toString());
        }
        System.out.println("被赦免者是"+list.get(0).toString());
    }

    public static void main(String[] args) {
        new Josephus(5,0,2);

    }
}

3.线性表的链式表示和实现

线性表的链式存储是用若干地址分散的存储单元存储数据元素,逻辑上相邻的数据元素在物理位置不一定相邻。
存储一个数据元素的存储单元至少包含两个部分——数据域和地址域,数据域中存储的是数据元素值,地址域存储的是后继元素的地址。

3.1 单链表

单链表是由一个个结点链接而成,如图:


单链表结构图.PNG

3.1.1 单链表的结点

单链表是由一个个结点链接而成,不同的链表之间的区别在与结点的不同。
声明单链表的类Node,代码如下:

public class Node<T> {
    /**数据域,保存数据元素*/
    public T data;
    /**地址域,引用后继结点,通过next链将两个结点链接起来*/
    public Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public Node() {
        this(null,null);
    }
}

成员变量next的数据类型是Node类自己,这个类型称为“自引用的类”。是指一个类声明包含一个引用当前类的对象的成员变量。

3.1.2 单链表的遍历操作

遍历单链表是指从第一个结点开始,沿着结点的next链,依次访问单链表中的每个结点,并且每个结点只访问一次。

Node<T> node = head;
while(node != null){
    node = node.next;
}

语句node = node.next是node移动到后继结点,此时结点间的链接关系没有改变。如果写为“node.next = node”,就变成node.next指向了自己,
改变了链接关系,并且丢失了后继结点,遍历就变成死循环

3.1.3 单链表的插入操作

对于单链表进行插入操作,只要改变结点间的链接关系,不需要移动数据元素。在单链表中插入一个结点,根据位置的不同分为:空表插入、头插入、中间插入、尾插入
1.空表插入、头插入

 head = new Node<T>(t,head);

空表插入也是头插入,当然也可以拆开来写:

if(head == null){
    //空表插入
    head = new Node<T>(t,null);
}else{
    //头插入
    Node<T> node = new Node<T>(t,null);
    node.next = head;
}

2.中间插入、尾插入
假设node指向了非空单链表中的某个结点,在node之后的插入q结点

    Node<T> node = new Node<T>(t,null);
    q.next=node.next; //q的后继结点是node的原后继结点
    node.next = q;//q作为node的新后继节点

若node指向最后一个结点,node.next == null,可以执行上述代码块。上面代码也可以精简为一句话:

p.next = new Node<T>(t,p.next);

3.1.4 单链表的删除操作

在单链表中删除指定结点,只要改变结点的next域就可以改变结点间的链接关系,不需要移动元素。根据结点的位置分为头删除、中间删除、尾删除
1.头删除
删除单链表第一个结点,只要使head指向其后继结点即可

head = head.next;

若单链表只有一个结点,则删除该结点后单链表为空表。

3.1.5 带结点的单链表

带头结点的单链表是在单链表的第一个结点之前增加一个特殊结点,称为头结点。头结点的作用是使所有链表(包括空表)的头指针非空,
并使对单链表的插入、删除操作不需要区分为空表或是否在第一个位置进行,从而与其他位置的插入、删除操作一致。

public class SinglyLinkedList<T> implements LList<T> {

    //头指针,指向单链表的头结点
    public Node<T> head;

    /**
     * 重写默认无参构造,构造一个单链表,并带有头结点,data和next都为null
     */
    public SinglyLinkedList() {
        head = new Node<T>();
    }

    /**
     * 浅拷贝构造函数
     * @param list
     */
    /*public SinglyLinkedList(SinglyLinkedList<T> list) {
        head = list.head;
    }*/

    /**
     * 深拷贝构造函数
     * @param list
     */
    public SinglyLinkedList(SinglyLinkedList<T> list) {
        this();
        Node<T> node = list.head.next;
        Node<T> rear = this.head;

        while (node != null){
            rear.next = new Node<T>(node.data,null);
            rear = rear.next;
            node = node.next;
        }
    }

    /**
     * 由指定数组中的多个对象构造单链表,采用尾插入构造单链表
     * @param element 数据元素
     */
    public SinglyLinkedList(T[] element) {
        this();
        Node<T> rear = head;
        for (int i = 0; i < element.length; i++) {
            rear.next = new Node<T>(element[i],null);
            rear = rear.next;
        }
    }

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

    @Override
    public int size() {
        int i = 0;
        Node<T> node = head.next;
        while (node != null){
            i++;
            node = node.next;
        }
        return i;
    }

    @Override
    public T get(int index) {
        if (index >= 0){
            Node<T> node = head.next;
            for (int i = 0; node != null && i<index; i++) {
                node = node.next;
            }
            if (node != null){
                return node.data;
            }
        }
        return null;
    }

    @Override
    public void set(int index, T t) {
        if (t == null){
            return;
        }
        if (index >= 0){
            Node<T> node = head.next;
            for (int i = 0; node != null && i<index; i++) {
                node = node.next;
            }
            if (node != null){
                node.data = t;
            }else{
                throw new IndexOutOfBoundsException(index+"");
            }
        }
    }

    @Override
    public void insert(int index, T t) {
        if (t == null){
            return;
        }
        Node<T> node = head;
        for (int i=0;node.next!=null&&index<i;i++){
            node = node.next;
        }
        node.next = new Node<T>(t,node.next);
    }

    @Override
    public void add(T t) {
        insert(Integer.MAX_VALUE,t);
    }

    @Override
    public T remove(int i) {
        if (i >= 0){
            Node<T> node = head;
            for (int j = 0; node.next != null && j<i ; j++) {
                node = node.next;
            }
            if (node.next != null){
                T old = node.data;
                node.next = node.next.next;
                return old;
            }
        }
        return null;
    }

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

    @Override
    public T search(T key) {
        ////在unit-10-search中实现
        return null;
    }

    @Override
    public String toString() {
        String string="(";
        Node<T> node = this.head.next;
        while (node != null) {
            string += node.data.toString();
            if (node.next != null){
                string += ",";
            }
            node = node.next;
        }

        return string+")";
    }
}

3.1.6 循环单链表

循环单链表就是链表最后的一个结点的next链保存单链表的头指针head值,形成一个环形结构。


循环单链表.PNG

构造方法为:

public class CirSinglyLinkedList<T> {
    
    public Node<T> head;
    
    public CirSinglyLinkedList(){
        this.head = new Node<T>();
        this.head.next = this.head;
    }

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

    @Override
    public String toString() {
        String string="(";
        Node<T> node = this.head.next;
        while (node != this.head) {
            string += node.data.toString();
            if (node.next != null){
                string += ",";
            }
            node = node.next;
        }

        return string+")";
    }
    
    //...其他方法参考同SinglyLinkedList.class
}

3.2双链表

双链表的每个结点有两个地址域,分别指向它的前驱结点和后继节点,结构如下:


循环双链表.PNG

创建双链表基类:

public class DLinkNode<T> {
    public T data;
    public DLinkNode<T> prev,next;

    public DLinkNode(T data, DLinkNode<T> prev, DLinkNode<T> next) {
        this.data = data;
        this.prev = prev;
        this.next = next;
    }

    public DLinkNode() {
        this(null,null,null);
    }
}

双链表的插入和删除操作与单链表不同,其他均与单链表相似
1.双链表的插入
在双链表中插入一个结点,既可插入指定结点之前,也可以插入在指定结点之后。

//插入指定结点之前,假定在结点node之前插入值为x的q结点
q = new DLinkNode<T>(x,node.prev,node);
node.prev.next = q;
node.prev = q;

//插入指定结点之后,假定在结点node之后插入值为x的q结点
q = new DLinkNode<T>(x,node,node.next);
if(node.next != null){
    //中间插入时执行
    node.prev.next = q;
}
node.prev = q;

2.双链表的删除
代码如下:

node.prev.next = node.next;
if(node.next != null){
    node.next.prev = node.prev;
}

3.3循环双链表

循环双链表是最后一个结点的next指向头结点,头结点的prev链指向最后一个结点。空双链表是后继结点指向自己的开始结点,开始结点指向自己的后继结点。


循环双链表.PNG

代码如下:

public class CirDoublyLinkedList<T> implements LList<T> {

    public DLinkNode<T> head;

    public CirDoublyLinkedList(DLinkNode<T> head) {
        this.head = new DLinkNode<T>();
        this.head.prev = head;
        this.head.next = head;
    }

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

    @Override
    public int size() {
        int i = 0;
        DLinkNode<T> node = head.next;
        while (node != null){
            i++;
            node = node.next;
        }
        return i;
    }

    @Override
    public T get(int index) {
        if (index >= 0){
            DLinkNode<T> node = head.next;
            for (int i = 0; node != null && i<index; i++) {
                node = node.next;
            }
            if (node != null){
                return node.data;
            }
        }
        return null;
    }

    @Override
    public void set(int index, T t) {
        if (t == null){
            return;
        }
        if (index >= 0){
            DLinkNode<T> node = head.next;
            for (int i = 0; node != null && i<index; i++) {
                node = node.next;
            }
            if (node != null){
                node.data = t;
            }else{
                throw new IndexOutOfBoundsException(index+"");
            }
        }
    }

    /**
     * 将对象t插入在序号为index结点上
     * 时间复杂度为O(n)
     * @param index 结点
     * @param t 对象
     */
    @Override
    public void insert(int index, T t) {
        //不能插入空对象
        if (t == null){
            return;
        }
        //寻找插入位置,当循环停止时,node指向index-1结点上
        DLinkNode<T> node = this.head;
        for (int i = 0;node.next != this.head && i<index;i++){
            node = node.next;
        }
        //插入在node结点上
        DLinkNode<T> linkNode = new DLinkNode<T>(t,node,node.next);
        node.next.prev = linkNode;
        node.next = linkNode;
    }

    /**
     * 在循环双链表最后添加结点
     * 时间复杂度为O(n)
     * @param t 对象
     */
    @Override
    public void add(T t) {
        if (t == null){
            return;
        }
        //使用尾插法,插入在头结点之前
        DLinkNode<T> linkNode = new DLinkNode<T>(t,head.prev,head);
        head.next.prev = linkNode;
        head.next = linkNode;
    }

    /**
     * 删除序号为i的结点
     * @param i
     * @return
     */
    @Override
    public T remove(int i) {
        if (i >= 0){
            DLinkNode<T> node = this.head.next;
            //定位到待删除的结点
            for (int j = 0;node != head&&j<i;j++){
                node = node.next;
            }

            if (node != head){
                //获得原对象
                T old = node.data;
                node.prev.next = node.next;
                //删除序号为i的结点
                node.next.prev = node.prev;
                return old;
            }
        }
        return null;
    }

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

    @Override
    public T search(T key) {
        //在unit-10-search中实现
        return null;
    }
}

链接:文档地址 源码地址

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