数据结构与算法之线性表(超详细顺序表、链表)

前言

经过前面数据结构与算法基础知识我么知道了数据结构的一些概念和重要性,那么咱们今天总结下线性表相关的内容。固然,我用本身的理解解分享给你们。java

其实说实话,可能不少人依然分不清线性表顺序表,和链表之间的区别和联系!node

  • 线性表:逻辑结构, 就是对外暴露数据之间的关系,不关心底层如何实现,数据结构的逻辑结构大分类就是线性结构和非线性结构而顺序表、链表都是一种线性表。
  • 顺序表、链表:物理结构,他是实现一个结构实际物理地址上的结构。好比顺序表就是用数组实现。而链表用指针完成主要工做。不一样的结构在不一样的场景有不一样的区别。

在Java中,你们都知道List接口类型,这就是逻辑结构,由于他就是封装了一个线性关系的一系列方法和数据。而具体的实现其实就是跟物理结构相关的内容。好比顺序表的内容存储使用数组的,而后一个get,set,add方法都要基于数组来完成,而链表是基于指针的。当咱们考虑对象中的数据关系就要考虑指针的属性。指针的指向和value。c++

下面用一个图来浅析线性表的关系。可能有些不太确切,可是其中能够参考,而且后面也会根据这个图举例。
image-20210104160901005git

线性表基本架构

对于一个线性表来讲。无论它的具体实现如何,可是它们的方法函数名和实现效果应该一致(即便用方法相同、达成逻辑上效果相同,差异的是运行效率)。线性表的概念与Java的接口/抽象类有那么几分类似。最著名的就是List的Arraylist和LinkedList,List是一种逻辑上的结构,表示这种结构为线性表,而ArrayList,LinkedList更多的是一种物理结构(数组和链表)。github

因此基于面向对象的编程思惟,咱们能够将线性表写成一个接口,而具体实现的顺序表和链表的类能够实现这个线性表的方法,提升程序的可读性,还有一点比较重要的,记得初学数据结构与算法时候实现的线性表都是固定类型(int),随着知识的进步,咱们应当采用泛型来实现更合理。至于接口的具体设计以下:算法

package LinerList;
public interface ListInterface<T> {    
    void Init(int initsize);//初始化表
    int length();
    boolean isEmpty();//是否为空
    int ElemIndex(T t);//找到编号
    T getElem(int index) throws Exception;//根据index获取数据
    void add(int index,T t) throws Exception;//根据index插入数据
    void delete(int index) throws Exception;
    void add(T t) throws Exception;//尾部插入
    void set(int index,T t) throws Exception;
    String toString();//转成String输出    
}

顺序表

顺序表是基于数组实现的,因此全部实现须要基于数组特性。对于顺序表的结构应该有一个存储数据的数组data和有效使用长度length.编程

还有须要注意的是初始化数组的大小,你能够固定大小,可是笔者为了可用性若是内存不够将扩大二倍。数组

下面着重讲解一些初学者容易混淆的概念和方法实现。这里把顺序表比做一队坐在板凳上的人。数据结构

插入操做

add(int index,T t) 架构

其中index为插入的编号位置,t为插入的数据,插入的流程为:

(1)从后(最后一个有数据位)向前到index依次后移一位,腾出index位置的空间

(2)将待插入数据赋值到index位置上,完成插入操做

image-20210104194146220

能够看得出若是顺序表很长,在靠前的地方若是插入效率比较低(插入时间复杂度为O(n)),若是频繁的插入那么复杂度挺高的。

删除操做

同理,删除也是很是占用资源的。原理和插入相似,删除index位置的操做就是从index+1开始向后依次将数据赋值到前面位置上,具体能够看这张图:

image-20210104201346674

代码实现

这里我实现一个顺序表给你们做为参考学习:

package LinerList;

public class seqlist<T> implements ListInterface<T> {
    private Object[] date;//数组存放数据
    private int lenth;
    public seqlist() {//初始大小默认为10
        Init(10);
    }

    public void Init(int initsize) {//初始化
        this.date=new Object[initsize];
        lenth=0;        
    }
    public int length() {        
        return this.lenth;
    }

    public boolean isEmpty() {//是否为空
        if(this.lenth==0)
            return true;
        return false;
    }

    /*
     * * @param t    
     * 返回相等结果,为-1为false
     */
    public int ElemIndex(T t) {
        // TODO Auto-generated method stub
        for(int i=0;i<date.length;i++)
        {
            if(date[i].equals(t))
            {
                return i;
            }
        }
        return -1;
    }

    /*
     *得到第几个元素
     */
    public T getElem(int index) throws Exception {
        // TODO Auto-generated method stub
        if(index<0||index>lenth-1)
            throw new Exception("数值越界");
        return (T) date[index];
    }
    
    public void add(T t) throws Exception {//尾部插入
         add(lenth,t);
    }

    /*
     *根据编号插入
     */
    public void add(int index, T t) throws Exception {
        if(index<0||index>lenth)
            throw new Exception("数值越界");
        if (lenth==date.length)//扩容
        {
            Object newdate[]= new Object[lenth*2];
            for(int i=0;i<lenth;i++)
            {
                newdate[i]=date[i];
            }
            date=newdate;
        }
        for(int i=lenth-1;i>=index;i--)//后面元素后移动
        {
            date[i+1]=date[i];
        }
        date[index]=t;//插入元素
        lenth++;//顺序表长度+1
        
    }

    public void delete(int index) throws Exception {
        if(index<0||index>lenth-1)
            throw new Exception("数值越界");
        for(int i=index;i<lenth;i++)//index以后元素前移动
        {
            date[i]=date[i+1];
        }
        lenth--;//长度-1    
    }

    @Override
    public void set(int index, T t) throws Exception {
        if(index<0||index>lenth-1)
            throw new Exception("数值越界");
        date[index]=t;
    }
    public String  toString() {
        String vaString="";
        for(int i=0;i<lenth;i++)
        {
            vaString+=date[i].toString()+" ";
        }
        return vaString;
        
    }
}

链表

学习c/c++的时候链表应该是不少人感受很绕的东西,这个很大缘由可能由于指针,Java虽然不直接使用指针可是咱们也要理解指针的原理和运用。链表不一样于顺序表(数组)它的结构像一条链同样连接成一个线性结构,而链表中每个节点都存在不一样的地址中,链表你能够理解为它存储了指向节点(区域)的地址,可以经过这个指针找到对应节点。

对于物理存储结构,地址之间的联系是没法更改的,相邻就是相邻。但对于链式存储,下一位的地址是上一个主动记录的,能够进行更改。这就比如亲兄弟从出生就是同姓兄弟,而咱们在成长途中最好的朋友可能会因为阶段性发生一些变化!

就如西天取经的唐僧、悟空、八戒、沙和尚。他们本无联系,但结拜为师徒兄弟,你问悟空他的师父它会立马想到唐僧,由于五指山下的约定。

image-20210104215538219

基本结构

对于线性表,咱们只须要一个data数组和length就能表示基本信息。而对于链表,咱们须要一个node(head头节点),和length分别表示存储的节点数据和链表长度,这个节点有数据域指针域。数据域就是存放真实的数据,而指针域就是存放下一个node的指针,其具体结构为:

class node<T>{
    T data;//节点的结果
    node next;//下一个链接的节点
    public node(){}
    public node(T data)
    {
        this.data=data;
    }
    public node(T data, node next) {
        this.data = data;
        this.next = next;
    } 
}

带头结点链表VS不带头结点链表

有不少人会不清楚带头结点和不带头结点链表的区别,甚至搞不懂什么是带头结点和不带头结点,我给你们阐述一下:

带头结点:head指针始终指向一个节点,这个节点不存储有效值仅仅起到一个标识做用(至关于班主任带学生)

不带头结点:head指针始终指向第一个有效节点,这个节点储存有效数值。

那么带头结点和不带头结点的链表有啥区别呢?

查找上:无大区别,带头结点须要多找一次。

插入上:非第0个位置插入区别不大,不带头结点的插入第0号位置以后须要从新改变head头的指向。

image-20210104231252005

删除上:非第0个位置删除区别不大,不带头结点的删除第0号位置以后须要从新改变head头的指向。

头部删除(带头节点):带头节点的删除和普通删除同样。直接head.next=head.next.next,这样head.next就直接指向第二个元素了。第一个就被删除了

头部删除(不带头节点):不带头节点的第一个节点(head)就存储有效数据。不带头节点删除也很简单,直接将head指向链表中第二个node节点就好了。即:head=head.next

image-20210104231732318

总而言之:带头结点经过一个固定的头可使链表中任意一个节点都同等的插入、删除。而不带头结点的链表在插入、删除第0号位置时候须要特殊处理,最后还要改变head指向。二者区别就是插入删除首位(尤为插入)固然我是建议你之后在使用链表时候尽可能用带头结点的链表避免没必要要的麻烦。

带头指针VS带尾指针

基本上是个链表都是要有头指针的,那么头尾指针是个啥呢?

头指针: 其实头指针就是链表中head节点,成为头指针。

尾指针: 尾指针就是多一个tail节点的链表,尾指针的好处就是进行尾插入的时候能够直接插在尾指针的后面,而后再改变一下尾指针的顺序便可。

image-20210105111135281

可是带尾指针的单链表若是删除尾的话效率不高,须要枚举整个链表找到tail前面的那个节点进行删除。

插入操做

add(int index,T t)
其中index为插入的编号位置,t为插入的数据,在带头结点的链表中插入在任何位置都是等效的。
加入插入一个节点node,根据index找到插入的前一个节点叫pre。那么操做流程为

1. `node.next=pre.next`,将插入节点后面先与链表对应部分联系起来。此时node.next和pre.next一致。
2. `pre.next=node` 将node节点插入到链表中。

image-20210105001736232

固然,不少时候链表须要插入在尾部,若是频繁的插入在尾部每次枚举到尾部的话效率可能比较低,可能会借助一个尾指针去实现尾部插入。

删除操做

按照index移除(主要掌握):delete(int index)

本方法为带头结点普通链表的通用方法(删除尾也同样),找到该index的前一个节点pre,pre.next=pre.next.next

image-20210105161038897

代码实现

在这里我也实现一个单链表给你们做为参考使用:

package LinerList;

class node<T>{
    T data;//节点的结果
    node next;//下一个链接的节点
    public node(){}
    public node(T data)
    {
        this.data=data;
    }
    public node(T data, node next) {
        this.data = data;
        this.next = next;
    }
   
}
public class Linkedlist<T> implements ListInterface<T>{

    node head;
    private int length;
    public Linkedlist() {
        head=new node();
        length=0;
    }
    public void Init(int initsize) {
        head.next=null;
        
    }

    public int length() {
        return this.length;
    }

    
    public boolean isEmpty() {
        if(length==0)return true;
        else return false;
    }

    /*
     * 获取元素编号
     */
    public int ElemIndex(T t) {
        node team=head.next;
        int index=0;
        while(team.next!=null)
        {
            if(team.data.equals(t))
            {
                return index;
            }
            index++;
            team=team.next;
        }
        return -1;//若是找不到
    }

    @Override
    public T getElem(int index) throws Exception {
        node team=head.next;
        if(index<0||index>length-1)
        {
            throw new Exception("数值越界");
        }
        for(int i=0;i<index;i++)
        {
            team=team.next;
        }
        return (T) team.data;
    }


    public void add(T t) throws Exception {
        add(length,t);
        
    }
    //带头节点的插入,第一个和最后一个同样操做
    public void add(int index, T value) throws Exception {
        if(index<0||index>length)
        {
            throw new Exception("数值越界");
        }
        node<T> team=head;//team 找到当前位置node
        for(int i=0;i<index;i++)
        {
             team=team.next;
        }
        node<T>node =new node(value);//新建一个node
        node.next=team.next;//指向index前位置的下一个指针
        team.next=node;//本身变成index位置    
        length++;
    }
    

    @Override
    public void delete(int index) throws Exception {
        if(index<0||index>length-1)
        {
            throw new Exception("数值越界");
        }
        node<T> team=head;//team 找到当前位置node
        for(int i=0;i<index;i++)//标记team 前一个节点
        {
             team=team.next;
        }
        //team.next节点就是咱们要删除的节点
        team.next=team.next.next;
        length--;
    }

    @Override
    public void set(int index, T t) throws Exception {
        // TODO Auto-generated method stub
        if(index<0||index>length-1)
        {
            throw new Exception("数值越界");
        }
        node<T> team=head;//team 找到当前位置node
        for(int i=0;i<index;i++)
        {
             team=team.next;
        }
        team.data=t;//将数值赋值,其余不变
        
    }

    public String toString() {
        String va="";
        node team=head.next;
        while(team!=null)
        {
            va+=team.data+" ";
            team=team.next;
        }
        return va;
    }

}

总结

你可能会问这个是否正确啊,那我来测试一下:

image-20210105164129930

这里的只是简单实现,实现基本方法。链表也只是单链表。完善程度还能够优化。能力有限, 若是有错误或者优化的地方还请大佬指正

单链表查询速度较慢,由于他须要从头遍历,若是在尾部插入,能够考虑设计带尾指针的链表。而顺序表查询速度虽然快可是插入很费时费力,实际应用根据需求选择

Java中的Arraylist和LinkedList就是两种方式的表明,不过LinkedList使用双向链表优化,而且JDK也作了大量优化。因此你们不用造轮子,能够直接用,可是手写顺序表、单链表仍是颇有学习价值的。

单链表搞懂了,双链表也就不远了(下集预告)!

原创公众号: bigsai
文章已收录在 全网都在关注的数据结构与算法学习仓库 欢迎star
相关文章
相关标签/搜索