Data Structures and Algorithms (vii) single chain - chain linear table storage structure

Single list


The form of data stored in the computer, there are two, the data structure is a sequential storage element address stored in contiguous memory locations, the write our previous linear form, the stack order, are sequentially stored in the circular queue structure implemented based on dynamic array this structure occupies a contiguous memory locations, more waste of space, any data elements stored in the storage unit, the space can be fully utilized, this storage structure is linked storage structure.

The chain storage structure does not reflect the logical relationships between data elements, it is necessary to store data element with a pointer address, so that the address can be by the associated data element position, so that a lot of flexibility to the chain store, where the data exists is not important, as long as there is a pointer to store the address corresponding to find it.

In order to express a logical relationship between each data element of a data element B and its immediate successor, a data element, in addition to its own stored information, the need to store information indicating its successor. We store the domain information data elements called a data field, the field stores immediately subsequent position is referred pointer field. Pointer information stored in the domain called pointer or a chain. Storing image data elements composed of two pieces of information of a called node (Node).
Here Insert Picture Description
n nodes (a memory mapped) into a link list, i.e. linear form of the chain storage structure, since each node in this linked list contains a pointer field only, so called single chain.
Here Insert Picture Description

The first node and the head pointer

Refers to the first node the first node in the linked list, there are sub-header real and virtual nodes of the first node
in a realistic head node: its first node for storing data
Here Insert Picture Description

Virtual head node: a node that first allowed to store the data
Here Insert Picture Description
the way, the head and tail pointers are merely a reference variable, namely the pointer list head node and the last node concerned.

In fact, we found the list and order form and the basic operation is almost, but the element into taking in terms of points, so for a single list or you can get it on the List interface, you can override the abstract methods interface.

The following table achieve a linear chain structure stored in the Java language, virtual head node mode, Storage Structure nodes need this thing, put it in a single class, as a part of two internal classes, class definitions node , a pointer data field and the data field next. Some are based on the basic operation of the linked list node implemented.

package DS02.动态链表;

import DS01.动态数组.List;

import java.util.Iterator;
//用动态链表实现线性表  链表
    //头插法:像栈
    //尾插法:像队列
    //头插尾插结合

public class LinkedList<E> implements List<E> {
    private Node head;  //链表的头指针
    private Node rear;  //链表的尾指针
    private int size;   //链表的元素个数(结点个数)

    //构造函数
    public LinkedList(){
        head=new Node();
        //刚开始给一个空的结点
        rear=head;      //头尾结点一样,即空表   再加结点头指针不动尾指针动就ok
    }

    //内部类
    class Node{   //链表内部的东西,内部类私有,外部不需要知道结点的存在
        E data;  //数据域      类型由外界决定
        Node next; //指针域
        //构造函数
        Node(){
            this(null,null);
        }
        Node(E data,Node next){
            this.data=data;
            this.next=next;
        }

        @Override
        public String toString() {
            return data.toString();     //由调用者决定,结点的toString
        }
    }

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

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

这里比较重要的是,单链表中插入和删除元素,先对插入元素进行分析,有头插法,尾插法,头插尾插结合还有一般插入,分别分析其过程:

  • 头插法: 先把虚拟头结点的指针域存放的物理地址给新结点的指针域,头结点的指针域指向新结点的地址,就完成了表头插入元素的操作
  • 尾插法:先把新结点的物理地址给尾结点的指针域,新结点的地址给尾指针
  • 头插尾插结合:头插和尾插在链表为空时,即插入第一个元素时实现方法是一样的,后面的元素进入就分头插尾插两方面
  • 一半插入:借助指针p遍历找要插入的位置,把p指针指向结点下一结点的地址给新结点的指针域,再将新结点的地址给p结点的指针域

代码实现如下:(代码中没有头尾结合插入的代码)

//插入元素
    @Override
    public void add(int index, E e) {
        if(index<0||index>size){    //角标越界
            throw new IllegalArgumentException("角标越界");
        }
        //创建新的结点
        Node n=new Node();
        n.data=e;   //结点的数据域存e
        if(isEmpty()){  //空表状态 特殊处理
            head.next=n;
            rear=n;
        }else if(index==0){     //头插法
            n.next=head.next;
            head.next=n;
        }else if(index==size){  //尾插法
            rear.next=n;
            rear=n;
        }else{                  //中间插入
            Node p=head;
            for(int i=0;i<index;i++){
                p=p.next;
            }
            n.next=p.next;    //把p指针指向结点下一结点的地址给新结点的指针域
            p.next=n;   //新结点的地址给p结点的指针域
        }
        size++;    //有效元素+1
    }

    @Override
    public void addFirst(E e) {
        add(0,e);
    }

    @Override
    public void addLast(E e) {
        add(size,e);
    }
    

获取角标对应元素,对一些特殊情况进行判断,链表为空,角标越界时抛异常

 //获取角标对应的元素
    @Override
    public E get(int index) {
        if(isEmpty()){
            throw new IllegalArgumentException("空表");
        }
        if(index<0||index>size){
            throw new IllegalArgumentException("角标越界");
        }
        if(index==0){
            return head.next.data;
        }else if(index==size-1) {
            return rear.data;
        }else{
            Node p=head;    //借助指针p找到index对应结点
            for(int i=0;i<=index;i++){
                p=p.next;
            }
            return p.data;  //返回指针p处结点数据域
        }

    }

    @Override
    public E getFirst() {
        return get(0);
    }

    @Override
    public E getLast() {
        return get(size-1);
    }

修改元素和查看是否包含该元素方法的前提是找到元素,借助指针p遍历链表查找角标index对应元素,实现代码如下:

 @Override
    public void set(int index, E e) {
        if(isEmpty()){
            throw new IllegalArgumentException("空表");
        }
        if(index<0||index>size){
            throw new IllegalArgumentException("角标越界");
        }
        Node p=head;
        for(int i=0;i<=index;i++){     //修改元素,先找再改
            p=p.next;
        }
        p.data=e;
    }
	
    @Override
    public boolean contains(E e) {
        return find(e)!=-1;
    }

    @Override
    public int find(E e) {
        Node p=head;
        for(int i=0;i<=size;i++){
            p=p.next;
            if(p.data.equals(e)){   //p指针结点处存放的元素与e比较
                return i;       //返回角标
            }
        }
        return -1;
    }

删除元素和插入元素一样分头删、尾删和中间删,其实三种方式的删除方法一样,都是断了要删除结点与其他结点的联系,让其被回收,下面具体讨论:

  • 头删:创建变量del存放要删除的结点,也就是头指针处结点,把要删除元素结点的元素给变量ret,用于返回。让头结点指针域指向删除元素的下一个,再将del的指针域指向空,断了del和其他结点的联系,del会被回收器回收

  • 尾删:要删除元素,要先找到要删除元素的前一个,将它的指针域指向空,将尾结点指针域指向空,尾结点会被回收器回收

  • 中间删:先将要删除的元素用变量del存放,中间删除也是找要删除结点的前一个,借助指针p遍历寻找,将它的指针域指向del的下一个结点地址,即跳过del,再将del的指针域指向空,del被回收

还有一种特殊情况,就是链表内只有一个元素的情况,把它单独考虑一下,减小时间复杂度,在执行删除后记得让size-1

//删除结点
    @Override
    public E remove(int index) {
        if(isEmpty()){
            throw new IllegalArgumentException("空表");
        }
        if(index<0||index>=size){
            throw new IllegalArgumentException("角标越界");
        }
        E ret=null;
        if(size==1){    //只有一个结点的情况
            ret=rear.data;
            head.next=null;
            rear=head;
        }else if(index==0){     //要删除元素在表头
            Node del=head.next;
            ret=del.data;
            head.next=del.next;
            del.next=null;
            del=null;
        }else if(index==size-1){    //要删除元素在表尾
            Node p=head;
            while(true){
                if(p.next!=rear){
                    p=p.next;
                }else{
                    break;
                }
            }
            ret=p.next.data;
            p.next=null;
            rear=p;
        }else{                  //要删除元素在中间的情况
            Node p=head;
            for(int i=0;i<index;i++){
                p=p.next;
            }
            Node del=p.next;
            ret=del.data;
            p.next=del.next;
            del.next=null;
            del=null;
        }
        size--;           //有效元素-1
        return ret;        //返回被删除的元素
    }

    @Override
    public E removeFirst() {
        return remove(0);
    }

    @Override
        public E removeLast() {
            return remove(size-1);
    }

    @Override
    public void removeElement(E e) {
        int index=find(e);
        if(index!=-1){
            remove(index);
        }else{
            throw new IllegalArgumentException("找不到");
        }
    }

清空链表和以字符串形式打印,方便测试方法的正确性

 @Override
    public void clear() {   //清空表
        head.next=null;
        rear=head;
        size=0;
    }

    //按数组的格式打印
    @Override
    public String toString() {
        StringBuilder sb=new StringBuilder();
        sb.append("LinkedList: "+size+"\n");
        sb.append('[');
        if(isEmpty()){
            sb.append(']');
        }else{
            Node p=head;
            while(true){
                if(p.next!=rear){
                    p=p.next;
                    sb.append(p.data);
                    sb.append(',');
                }else{
                    sb.append(rear.data);
                    sb.append(']');
                    break;
                }
            }
        }
        return sb.toString();
    }

迭代器,写内部类,创建对象LinkedListIterator,目的是可以循环输出链表内的元素,并支持foreach循环。

 //迭代器
    @Override
    public Iterator<E> iterator() {
        return new LinkedListIterator();
    }
    //内部类
    public class LinkedListIterator implements Iterator<E>{
        private Node p=head;

        @Override
        public boolean hasNext() {
            return p.next!=null;
        }

        @Override
        public E next() {
            p=p.next;
            return p.data;
        }
    }

Of course, each writing a method, we are to be tested in the class, to correct errors in a timely manner, to develop a good habit to write code.

Published 70 original articles · won praise 56 · views 1983

Guess you like

Origin blog.csdn.net/qq_43624033/article/details/103588794