线性表(List)是一种线性结构。其特点是数据元素直线的线性关系。

1.线性表抽象类定义

    public abstract class AbsList<T> implements Iterable<T>,List<T>{
    protected int length;
    abstract public  T  get(int i);     //返回第i(i≥0)个元素
    abstract public  boolean set(int i, T x); //设置第i个元素值为x
       //查找,返回首次出现的关键字为key元素
    abstract public  int indexOf(int begin,int end,T o); 
    abstract public  void    add(int i, T x);  //插入x作为第i个元素    
    abstract public  void    clear();  
    abstract public  T  remove(int i);   //删除第i个元素并返回被删除对象
    abstract public Iterator<T> iterator();//返回一个迭代器
    public boolean isEmpty(){ return length==0;}  //判断线性表是否空
            public int   length(){  return length;}    //返回线性表长度
            public void  add(T x){  add(length,x); }   //在线性表最后插入x元素
            public void  append(T x){  add(length,x); }
    public int   indexOf(T o){
        return indexOf(0,length,o);
    }
    public int   indexOf(int begin,T o){
        return indexOf(begin,length,o);
    }
    public  T  remove(T o){       //删除第i个元素并返回被删除对象
        return remove(indexOf(o));
    }
}

1.1初始化:

public Seqlist(int initlen){//initlen初始化容量
    if(initlen<0) initlen = 16;
    length = 0;
    incrementSize = 0;
    data = new Object[initlen]; 
}
//默认构造函数
public Seqlist(){
    this(16);
}
//用数组elem初始化顺序表
public Seqlist(T[] elem){
    length = elem.length;
    incrementSize = 0;
    data=Arrays.copyOf(elem,length);//将elem全部存入data
}

1.2容量管理

public void setCapacity(int newsize){
    data=Arrays.copyOf(data,newsize);
}
//获取顺序表的最大容量
public int getCapacity(){
    return data.length;
}
public void setIncr(inc){
    incrementSize=inc;
}
//内部使用,顺序表自动扩容
private void grow(){
    int newsize=data.length+incrementSize;
    data=Arrays.copyOf(data.newsize);
}

1.3数据存取

public T get(int i)
{
    if(i<0 || i>length - 1)
        return null;
    return (T)data[i];
}
public boolean set(int i,T x)
{
    if(i<0 || i>length - 1)
        return false;
    else
    {
        data[i] = x;
        return true;
    }
}  

1.4指定位置插入元素

public void add(int i,T x)
{
    if(length == data.length)
        grow();
    if(i<0)
        i = 0;
    if(i>length)
        i = length;
    for(int j=length-1;j>=i;j--)
        data[j+1] = data[j];
    data[i] = x;
    length++;
}
public void add(T x)
{
    if(length == data.length)
        grow();
    data[length] = x;
    length++;
}

1.5删除顺序表元素

//删除下标为i的元素
public T remove(int i)
{
    if(i<0 || i>length -1)
        throw new IndexOutOfBoundsException("下标越界 i="+i);
    T olddata = (T)data[i];
    for(int j=i;j<length-1;j++)
        data[j] = data[j+1];
    data[--length] = null;
    return olddata;
}
//删除值为o的元素
public T remove(T o)
{
    int i =indexOf(o);
    return remove(i);
}
//清空顺序表
public void clear()
{
    for(int i=0;i<length;i++)
        remove(i);
}

1.6查找值为o的数据元素的下标,使用顺序查找算法

//注意:我们许可查到的是null
public int indexOf(T o)
{
    if(o == null)
    {
        for(int i=0;i<length;i++)
            if(data[i] == null)
                return i;
    }
    else
    {
        for(int i=0;i<length;i++)
            if(compare(o,(T)data[i]) == 0)
                return i;
    }
    return -1;
}

1.7顺序表中元素有序插入与插入排序

//T数据元素比较方法
protected int compare(T a,T b)
{
    if(a instanceof Comparable && b instanceof Comparable)
        return ((Comparable) a).compareTo((Comparable)b);
    else 
        return ((String)a).compareTo((String)b);
}

instanceof运算符的前一个操作数是一个引用类型变量,后一个操作数是一个类(接口),用于判断前面的对象是否是后面的类,或子类、实现类的实例。若是,返回true;反之,返回false。

如果a,b均是comparable容器已实现的类型,则用comparable直接比较。
否则,转换为string类比较

//内部使用,有序地插入x
privated void insertOrder(int end,T x)
{
    if(length == data.length)
        grow();
    int k;
    for(k=end-1;k>=0;k--)
    {
        if(compare(x,(T)data[k])<0)
            data[k+1] = data[k];
        else
            break;
    }
    data[k+1]=x;
}
public void addSort(T x)
{
    insertOrder(length,x);
    length++;
}
//顺序表排序算法
public void sort()
{
    for(int i=0;i<length;i++)
        insertOrder(i,(T)data[i]);
}

addSort时间复杂度为O(n) sort为图片描述

1.8顺序表转换为数组

public Object[] toArray()
{
    return Arrays.copyOf(this.data, this.length);
}
public T[] toArray (T[] a)
{
    if(a.length<length)
        //创建一个与数组a运行类型相同的新数组,长度可以是0
        return (T[])Arrays.copyOf(this.data, this.length,a.getClass());
    System.arraycopy(this.data, 0, a, 0, this.length);
    if(a.length > this.length)
        a[length] = null;
    return a;
}    

1.9顺序表转换为字符串

public String toString()
{
    StringBuilder strb = new StringBuilder();
    strb = strb.append("(");
    for(int i=0;i<length-1;i++)
    {
        strb = strb.append(data[i].toString()+",");
    }
    strb=strb.append(data[length-1]+")");
    String s = new String(strb);
    strb=null;
    return s;
}

2单链表的定义及其应用

2.1单链表的概念

单链表是一个用指向后继元素的指针将具有线性关系的节点链接起来,最后一个节点的后继指针为空指针。

图片描述

节点:

class  Lnode<T>{
    public T data;
    public Lnode<T> next;
}

由于节点类由data、next两部分组成,没有Comparable中数据类型的实现。故需改写Comparable中的方法。

节点类新增、改写几个方法:equals,comparaTo,toString

public  class Lnode<T> implements Comparable<Lnode<T>> {
    public T data;
    public Lnode<T> next;
    public Lnode(T key){
        data = key;
        next = null;
    }
    public Lnode(T key,Lnode<T> next){
           data = key;
           this.next = next;
    }
    //重写的三个方法
    public boolean equals (Object e){
        @SuppressWarnings("unchecked")
        Lnode<T> node =(Lnode<T>)e;
        return data.equals(node.data);
    }
    @SuppressWarnings("unchecked")
    public int compareTo(Lnode<T> e) {
        Comparable<T> x;
        if(data instanceof Comparable){
            x = (Comparable<T>)data;
            return (int)x.compareTo(e.data);
        }
        else throw new ClassCastException("类型无法比较");
    }
    public String toString(){
        return data.toString();
    }
}

单链表的定义(部分):

public class LinkList<T> extends AbsList<T> implements Iterable<T> {    
     Lnode<T> first,last;//头指针与尾指针
     Iterator<T> itr = null;//指向当前节点迭代器的指针
     public LinkList(){
         first = last = null; length = 0;
         this.itr = new LinkIterator();
     }
     public LinkList(int i){
         first = last = null; length = 0;
         if(i!=-1)
         this.itr = new LinkIterator();
     }
    
    private int compare(Lnode<T> a,Lnode<T> b)
     {
         return a.compareTo(b);
     }
    public void clear()
    {
        first=last=null;
        length=0;
    }
    public void removeAll()
    {
        clear();
    }
    //...
}

2.2单链表如何存取数据

首先用getNode(i)获取编号为i节点的引用

链表getNode(i)时间复杂度为O(n)。注意,在顺序表中,复杂度为O(1)

获得引用后,用get(i)取得data值,set(i,x)修改i号节点的值。

protected Lnode<T> getNode(int i)//getNode(i)获取编号为i节点的引用
{
    if(i<0 || i>length-1)
        return null;
    if(i==0)
        return first;
    Lnode<T> p= first;
    int j=0;
    while(p!=null&&j<i)
    {
        p=p.next;
        j++;
    }
    return p;
}
public T get(int i)//从引用中获取值
{
    Lnode<T> p=getNode(i);
    if(p==null)
        return null;
    else 
        return p.data;
}
public boolean set(int i,T x)//在引用中修改值
{
    Lnode<T> p=getNode(i);
    if(p==null)
        return false;
    else
    {
        p.data = x;
        return true;
    }
}

2.3向链表中插入元素

s=new Lnode<T>(x)

s是指向新结点的引用变量,x是被插入的值。
分四种情况讨论:

1.向空链表插入一个新节点

if(first==null){
    first = s;//直接将first指向s即可
    last = s;
}

2.在链表头结点之前插入新节点

s.next = first; first=s;

3.在链表中间插入新节
图片描述

s.next = p.next; p.next=s;

思考,两条语句能否调换位置? 答:不能,p.next=s,此时s未确定,p指向未知位置。

4.在链表尾部插入新节点

last.next=s; last=s;

完整的链表插入算法:

//注意:i是逻辑位置
public void add(int i,T x)
{
    Lnode<T>p,s;
    int j = i-1;
    s=new Lnode<T>(x,null);
    if(first==null||length==0)//1.向空链表插入一个新节点
    {
        first = s; last = s;
    }
    else if(j<0)//2.在链表头结点之前插入新节点
    {
        s.next = first; first = s;
    }
    else if(j>length-1)//4.在链表尾部插入新节点
    {
        last.next =s; last = s;
    }
    else //3.在链表中间插入新节
    {
        p=getNode(j);
        s.next=p.next;
        p.next=s;
    }
    length++;
}
//实际开发中,为方便使用,增加的函数
//重载add
public void add(T key){
    add(length,key);
}
public void addBack(T key)
{
    add(length,key);
}
public void addFront(T key)
{
    add(0,key);
}

2.4删除链表节点

分三种情况处理:

1.删除的是空链表(first==null)
链表为空,此时删除非法。

2.被删除的是头结点(i=0)
first = first.next;//即可实现
实际开发中,可能需要被删除的值,故:

p=first;
first = first.next;
return p.data;

3.被删除的节点在中间

q.next = p 
return p.data

图片描述

得知要删除的节点p后,如何准确找到指针q的位置呢

q=getNode(i-1);

完整的删除算法如下:

public T remove(int i)
{
    Lnode<T> p=removeNode(i);
    if(p!=null)
        return p.data;
    else 
        return null;
}
//删除逻辑第i节点,返回节点类型
protected Lnode<T> removeNode(int i)
{
 Lnode<T> p,q;
 if(first == null) return null;//1.删除的是空链表(first==null)
 if(i==0)//2.被删除的是头结点(i=0)
 {
     p = first; first = first.next; length--;
     return p;
 }
 if(i>=1&&i<=length-1)//3.被删除的节点在中间
 {
     q=getNode(i-1);
     p = q.next;
     q.next = p.next;
     if(p==last)last = q;
     length--;
     return p;
 }
 return null;
}

2.5查找节点

//在begin-end中查找节点
public int indexOf(int begin,int end,T key)
{
    Lnode<T>p = getNode(begin);
    int i = begin;
    while(p!=null&i<end)
    {
        if(p.data.equals(key)) return i;
        p = p.next;
        i++;
    }
    return i;
}
//在全链表中查找节点
public T search(T key)
{
    Lnode<T> p = getNode(0);
    while(p!=null)
    {
    if(p.data.equals(key)) return p.data;
    p = p.next;
    }
    return null;
}
//是否存在值为key的节点
public boolean contains(T key)
{
    if(indexOf(key)==-1)
        return false;
    else 
        return true;
}

2.5向链表中插入有序节点
分四种情况讨论
1.链表为空

first = s;first.next = null;

2.插入节点s的值小于头结点

s.next=first;

first=s;//first 指向新节点
3.插入节点s值大于尾节点

last.next=s;
last=s;

4.插入节点处于中间位置

p1=p2=first;
while(p2.next!=null){
    if(p1.data<s.data&&p2.data>s.data){
        将节点插入到p1之后;
        break;
    }else{
        p1=p2;p2=p2.next;
    }
}

单链表有序插入算法:

public void addSort(T x)
{
    Lnode<T> s=new Lnode<T>(x,null);
    insertOrder(s);
}

//核心算法

public void insertOrder(Lnode<T> s)
{
    Lnode<T> p1,p2;
    length++;
    if(first==null)//链表为空
    {
        first=s;
        last=first;
        return;
    }
    if(compare(s,first)<0)//插入到头结点以前
    {
        s.next=first;
        first=s;
        return;
    }
    if(compare(s,last)>=0)//插入到尾节点
    {
        last.next=first;
        last=s;
        return;
        
    }
    //插入到中央
    p2=first;
    p1=p2;
    while(p2!=null)
    {
        if(compare(s,p2)>0)//第一次执行必然s>p2
        {
            p1=p2;
            p2=p2.next;
        }
        else break;
    }
    s.next=p2;
    p1.next=s;
    return;
}

2.6链表排序
单链表的插入排序示例:

public void sort()
{
    LinkList<T> s1=new LinkList<T>();//有序链表
    Lnode<T> p;
    p=this.getNode(0);//取出无序链表的头结点,this是调用这个方法的链表
    while(p!=null)
    {
        s1.insertOrder(p);
        p=this.getNode(0);
    }
    this.first=s1.first;
    this.last=s1.last;
    this.length=s1.length;
}

总结:1.若是要在单链表中插入、删除一个节点,必须知道其前驱结点。
2.单链表不具有按序号随机处理的特点,只能从头指针一个一个顺序进行。

2.7链表转换为字符组/数组

public String toString(Lnode<T> first)
{
    String s;
    Lnode<T> p;
    p =first; s="(";
    while(p!=null)
    {
        s=s+p.data.toString();
        p = p.next;
    }
    return s= s+")\n";
}

public Object[] toArrays()
{
    Object[] a=new Object[length];
    Lnode<T> p=first;
    for(int i=0;i<length;i++)
    {
        a[i] = p.data;
        p=p.next;
    }
    return a;
}

3.循环链表与双向链表

3.1单循环链表
图片描述
具有单链表的特征,无需增加额外存储空间,对链表的处理更加灵活,整个链表构成一个环, 可以遍历已经访问过的元素。

在建立循环链表时,必须使最后一个节点的指针指向表头节点,而不是null。
在判断是否到表尾是.first==rear,而不是后继结点为null。

3.2双向链表

class DoubleNode(){
    public int data;
    public DoubleNode prior;
    public DoubleNode next;
}

1.双链表的插入运算

s= new DoubleNode;
s.data=e;
s.next=p.next;
p.next.prior=s;
p.next=s;
s.prior=p;//一定要注意顺序

图片描述

2.双链表的删除运算
直接断开节点间链接关系
删除s:

s.prior.next = s.next;
s.next.prior = s.prior;

4.额外补充:顺序表与链表的比较

一、顺序表的特点是逻辑上相邻的数据元素,物理位置相邻
并且,顺序表的存储空间需要预先分配
它的优点是:
(1)方法简单,各种高级语言中都有数组,易实现
(2)不用为表示节点间的逻辑关系而增加额外的存储开销。
(3)顺序表具有按元素序号随机访问的特点。
缺点:
(1)在顺序表中做插入、删除操作时,平均移动表中的一半元素,因此对n较大的顺序表效率低
(2)需要预先分配足够大的存储空间(难以估计),估计过大,可能会导致顺序表后部大量闲置;
预先分配过小,又会造成溢出。
二、在链表中逻辑上相邻的数据元素,物理存储位置不一定相邻,它使用**指针实现元素之间的
逻辑关系。并且,链表的存储空间是动态分配**的。
链表的最大特点是:
插入、删除运算方便。
缺点:
(1)要占用额外的存储空间存储元素之间的关系,存储密度降低
存储密度是指一个节点中数据元素所占的存储单元和整个节点所占的存储单元之比。
(2)链表不是一种随机存储结构,不能随机存取元素
三、顺序表与链表的优缺点切好相反,那么在实践应用中怎样选取存储结构呢?
通常有以下几点考虑:
(1)“MaxSize”分配,动态?静态?
当对线性表的长度或存储规模难以估计时,不宜采用顺序表。
当线性表的长度变化不大而且事先容易确定其大小时,为节省存储空间,
则采用顺序表作为存储结构比较适宜。
(2)基于运算的考虑(时间)
顺序存储是一种随机存取的结构,而链表则是一种顺序存取结构,
如果频繁按序号访问数据元素,显然顺表优于链表。
如果频繁增删,显然链表优于顺表。
(3)基于环境的考虑(语言)
顺序表容易实现,任何高级语言中都有数组类型;
链表的操作是基于指针的。相对来讲前者简单些,也用户考虑的一个因素。

总结: 通常“较稳定”的线性表,即主要操作是查找操作的线性表,适于选择顺序存储;
而频繁做插入删除运算的(即动态性比较强)的线性表适宜选择链式存储。

不得转载。


muddyway
10 声望3 粉丝