线性表之单链表的实现

单链表的实现

Posted by HuangCanCan on August 24, 2019

前言

今天主要是来模拟单链表的基本功能。

简介

链表是一系列的存储数据元素的单元,通过指针串接起来形成的,因此每个单元至少有两个域,一个域用于数据元素的存储,另一个域是指向其他单元的指针。 这里具有一个数据域和多个指针域的存储单元通常称为 结点(node)

一种最简单的结点结构如图所示,它是构成单链表的基本结点结构。在结点中数据域用来存储数据元素,指针域用于指向下一个具有相同结构的结点。 因为只有一个指针结点,称为单链表。

结点结构

特点

链表的第一个结点和最后一个结点,分别称为链表的 首结点和 尾结点。 尾结点的特征是其 next 引用为空(null)。 链表中每个结点的 next 引用都相当于一个指针,指向另一个结点, 借助这些 next 引用,我们可以从链表的首结点移动到尾结点。 在单链表中通常使用 head 引用来指向链表的首结点,由 head 引用可以完成对整个链表中所有节点的访问。

单链表

在单链表结构中还需要注意的一点是,由于每个结点的数据域都是一个 Object 类的对象,因此,每个数据元素并非真正如图中那样,而是在结点中的数据域通过一个 Object类的对象引用来指向数据元素的。 与数组类似,单链表中的结点也具有一个线性次序,即如果结点 P 的 next 引用指向结点 S,则 P 就是 S 的直接前驱,S 是 P 的直接后续。 单链表的一个重要特性就是只能通过前驱结点找到后续结点,而无法从后续结点找到前驱结点。

单链表的查询、添加、删除操作分析

查询操作

在单链表中进行查找操作,只能从链表的首结点开始,通过每个结点的 next 引用来依次访问链表中的每个结点以完成相应的查找操作。

例如:需要在单链表中查找是否包含某个数据元素 e,则方法是使用一个循环变量 p,起始时从单链表的头结点开始,每次循环判断 p所指结点的数据域是否和 e 相同,如果相同则可以返回 true,否则让p指向下一个节点,继续循环直到链表中所有结点均被访问,此时 p 为 null

单链表查询

关键操作:

  1. 起始条件:p = head
  2. 结束条件:
    找到:e.equals(p.getData())==true
    未找到: p == null
  3. p指向下一个结点: p = p.getNext();

缺点:逐个比较,频繁移动指针,导致效率低下

注意:如果要查询第i个元素的值,无法直接定位,也只能从首结点开始逐个移动到第i个结点,效率同样低下。

添加操作

在单链表中数据元素的插入,是通过在链表中插入数据元素所属的结点来完成的。
对于链表的不同位置,插入的过程会有细微的差别。
中间、末尾的添加过程其实是一样的,关键是在首部添加,会有不同,会改变整个单链表的起始结点。

单链表添加

以添加中间结点为例:
1.指明新结点的后继 s.setNext(p.getNext()); 或者 s.next = p.next
2.指明新结点的前驱(其实是指明前驱结点的后继是新结点) p.setNext(s) 或者 p.next = s;

添加节点不需要移动元素,只需要修改元素的指针即可,效率高。 但是如果需要先查询到添加位置再添加新元素,因为有逐个查询的过程,效率不高。

删除操作

单链表删除

在使用单链表实现线性表的时候,为了使程序更加简洁,我们通常在单链表的最前面添加一个哑元结点,也称为头结点。
在头结点中不存储任何实质的数据对象,其 next 域指向线性表中0号元素所在的结点,可以对空表、非空表的情况以及对首元结点进行统一处理,编程更方便,常用头结点。
一个带头结点的单链表实现线性表的结构图如图 所示。

带头结点单链表

单链表的实现

Node

/**
* 单链表的节点
*
* @ClassName Node
* @Description 单链表的节点
* @Author HuangCanCan
* @Date 2019/8/18 19:52
* @Version 1.0
**/
public class Node {
    /**
    * 要存储的数据
    */
    Object data;
    /**
    * 下一个节点
    */
    Node next;

    public Node(Object data) {
        this.data = data;
    }

    public Node() {
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}

SingleLinkedList

/**
* 单链表实现
* <p>
* 核心方法add(inde,e)
*
* @ClassName SingleLinkedList
* @Description 单链表实现
* @Author HuangCanCan
* @Date 2019/8/18 19:50
* @Version 1.0
**/
public class SingleLinkedList implements List {
    /**
    * 头结点,不存储数据,为了编程方便
    */
    private Node head = new Node();

    /**
    * 节点个数
    */
    private int size;

    @Override
    public int size() {
        return size;
    }

    @Override
    public Object get(int index) {
        /**
        * 链表和顺序表不一样,不能通过索引直接计算定位,而需要从头结点开始进行查找
        */
        // 定义一个指针p指向头节点
        Node p = head;
        for (int i = 0; i <= index; i++) {
            p = p.next;
        }
        return p.data;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public boolean contains(Object e) {
        return indexOf(e) >= 0;
    }

    @Override
    public int indexOf(Object e) {
        //返回第一次出现的指定元素的索引
        Node p = head.next;
        if (e == null) {
            for (int i = 0; i < size; i++) {
                if (p.data == null) {
                    return i;
                }
                //移动指针到下一个结点
                p = p.next;
            }
        } else {
            for (int i = 0; i < size; i++) {
                if (e.equals(p.data)) {
                    return i;
                }
                //移动指针到下一个结点
                p = p.next;
            }
        }
        return -1;
    }

    @Override
    public void add(int index, Object e) {
        //如果i位置错误报异常
        if (index < 0 || index > size) {
            throw new RuntimeException("数组指针越界异常:" + index);
        }

        //从head结点开始,找到index的前一个结点,
        Node p = head;
        for (int i = 0; i < index; i++) {
            p = p.next;
        }
        //新创建一个节点
        Node newNode = new Node();
        newNode.data = e;
        //指明新结点的直接后继结点
        newNode.next = p.next;
        //指明新结点的直接前驱结点
        p.next = newNode;
        size++;
    }

    @Override
    public void add(Object e) {
        //add(e)是添加元素到单链表末尾,是add(index,e)方法特例,可以如下:
        this.add(size, e);
    }

    @Override
    public boolean addBefore(Object obj, Object e) {
        //将数据元素 e 插入到元素 obj 之前,元素e是第一次出现的位置
        /**
        * 第一:找到元素obj的第一次出现位置的前一个节点
        * 第二:新建一个节点,指明该新结点的直接后续节点
        * 第三:把步骤一得到的节点,指明它的直接后续节点为步骤二新建的节点
        * 第四:新数组的长度为数组元素的个数+1
        */
        int index = indexOf(obj);
        if (index < 0) {
            return false;
        }
        add(index, e);
        return true;
    }

    @Override
    public boolean addAfter(Object obj, Object e) {
        //将数据元素 e 插入到元素 obj 之后
        int index = indexOf(obj);
        if (index < 0) {
            return false;
        }
        add(index + 1, e);
        return true;
    }

    @Override
    public Object remove(int index) {
        //删除线性表中索引为 index 的元素,并返回原数据元素
        if (index >= size) {
            throw new RuntimeException("索引越界异常:" + index);
        }
        Object oldValue = this.get(index);

        //得到index的前一个节点
        Node p = head;
        for (int i = 0; i < index; i++) {
            p = p.next;
        }
        //得到index的节点
        Node p2 = head;
        for (int i = 0; i <= index; i++) {
            p2 = p2.next;
        }
        p.next = p2.next;

        size--;
        return oldValue;
    }

    @Override
    public boolean remove(Object e) {
        //删除第一个与 e 相同的元素
        int index = this.indexOf(e);
        if (index < 0) {
            return false;
        }
        this.remove(index);
        return true;
    }

    @Override
    public Object replace(int index, Object e) {
        //替换索引为 index 的数据元素为 e,返回原数据元素
        if (index >= size) {
            throw new RuntimeException("索引越界异常:" + index);
        }
        Object oldValue = this.get(index);

        Node p = head;
        for (int i = 0; i <= index; i++) {
            p = p.next;
        }
        p.data = e;

        return oldValue;
    }

    @Override
    public String toString() {
        if (size == 0) {
            return "[]";
        }
        StringBuilder builder = new StringBuilder("[");
        Node p = head.next;
        for (int i = 0; i < size; i++) {
            if (i != size - 1) {
                builder.append(p.data + ",");
            } else {
                builder.append(p.data);
            }
            //移动指针到下一个结点
            p = p.next;
        }
        builder.append("]");
        return builder.toString();
    }
}

总结

链表和顺序表不一样,不能通过索引直接计算定位,而需要从头结点开始进行查找。
一个简单的结点由一个数据域和一个指针域组成。
为了使程序更加简洁,通常在单链表的最前面添加一个头结点。
头节点的数据域为NULL,尾节点的指针域为NULL。
单链表查询,只能从链表的头结点开始,通过每个结点的 next 引用来依次访问链表中的每个结点。
添加、删除节点不需要移动元素,只需要修改节点相应的指针指向即可。