データ構造とアルゴリズム(VII)単鎖 - テーブル記憶構造リニアチェーン

単一のリスト


データ構造は、連続したメモリ位置に格納された順次記憶素子のアドレスであり、ライト我々の以前の線状、スタック順序は、順次、循環キュー構造に格納されたコンピュータに格納されるデータの形式、二つがあるが、動的配列に基づいて実施しましたこの構造は、連続したメモリ位置、空間のより多くの廃棄物、記憶部に記憶された任意のデータ要素は、空間を完全に利用することができ、このストレージ構造が連結されるストレージ構造を占有します。

データ要素間の論理関係を反映していない鎖の記憶構造は、それがアドレスは、関連するデータ要素位置によって行うことができるように、ポインタアドレスとストアデータ要素に必要であるので、データが存在するチェーン店に多くの柔軟性限り、それを見つけるために、対応するアドレスを格納するポインタがある限り、重要ではありません。

データ要素Bとその直接の後継、データ要素、独自の記憶された情報に加えて、その後継者を示す情報を格納する必要の各データ要素間の論理的な関係を表現するために。我々は、データフィールドと呼ばれるドメイン情報データ要素、直後の位置がポインタフィールドと呼ばれるフィールドストアを格納します。ポインタ情報は、ポインタ又はチェーンと呼ばれる領域に記憶されています。呼ばれるノード(ノード)の二つの情報からなる画像データ要素を格納します。
ここに画像を挿入説明
n個のノード(メモリマップされた)リンクリストに、チェーン・ストレージ構造の、すなわち直線状、このリンクされたリスト内の各ノードがそう単鎖と呼ばれる、唯一のポインタフィールドを含むからです。
ここに画像を挿入説明

第1のノードと先頭ポインタ

リンクされたリストの最初のノードへの最初のノードを参照し、サブヘッダーの実際の第1のノードの仮想ノードが存在する
現実的なヘッドノードでは、データを記憶するための最初のノード
ここに画像を挿入説明

仮想ヘッドノード:最初のデータを記憶させノード
ここに画像を挿入説明
の方法は、頭部と尾ポインタは、単に参照変数、即ちポインタリストのヘッドノードと関係する最後のノードです。

実際には、我々はリストと注文フォームと基本的な動作はほとんどですが、ポイントの面で取っへの要素なので、単一のリストのためか、Listインタフェース上でそれを得ることができ、あなたは抽象メソッドインタフェースを無効にすることができました。

次の表は、Java言語に保存されているリニア鎖構造を達成するため、仮想ヘッドノードモードは、ストレージ構造のノードはこの事を必要とし、二つの内部クラス、クラス定義のノードの一部として、単一のクラスに入れて次に、ポインタデータフィールド及びデータ・フィールド。いくつかは実装されたリンクリストのノードの基本的な操作に基づいています。

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;
        }
    }

もちろん、それぞれの方法を書いて、我々は、書き込みコードに良い習慣を開発するために、タイムリーに正確なエラーのために、クラスでテストすることになっています。

公開された70元の記事 ウォン称賛56 ビュー1983

おすすめ

転載: blog.csdn.net/qq_43624033/article/details/103588794
おすすめ