Java数据结构与算法之顺序表的实现

本文深入探讨了顺序表的底层实现原理,通过手写一个简易版的ArrayList,详细解析了其增删改查操作的实现过程。文章重点介绍了动态扩容机制,以及如何在保证数据连续性的前提下进行高效的数据搬移。

一、前言

上一篇中内容中,我们从存储结构、性能以及试用场景几个方面分析了数组这种最基本的数据结构。顺序表的也是由数组实现的。在实际的开发过程中,我们也经常使用一个与数组类似的顺集合容器—ArrayList。ArrayList的底层实现就是数组,但是它把数组的特性进行了封装,调用起来更方便,同时优化了数组的痛点(不支持自动扩容)。下面我们就利用上一篇学到的知识,试着完成一个简单的顺序表。如图:

                             

先简单回顾下数组的特点:数组是一种线性表数据结构,是一组连续的内存空间(这也是随机访问的重要切入点),存储的数据类型相同。(https://blog.csdn.net/anyway8090/article/details/89428804)

下面来看一个接口MyList,接口中包含了容器类的大部分方法,包括增删改查的方法也就是我们自己手写的手写的MyArrayList的顶级接口。

package cn.com.arrayList;

/**
 * 顺序表顶级接口
 * @author Administrator
 */
public interface MyList<T> {

	/**
	 * 判断顺序表是否为空
	 */
	boolean isEmpty();
	/**
	 * 顺序表大小
	 */
	int size();
	/**
	 * 获取元素
	 * @param index
	 * @return
	 */
	T get(int index);
	/**
	 * 设置某个元素的值
	 * @param index
	 * @param data
	 * @return
	 */
	T set(int index,T data);
	/**
	 *添加元素
	 * @param data
	 * @return
	 */
	boolean add(T data);
	/**
	 * 在指定位置添加元素
	 * @param index
	 * @param data
	 * @return
	 */
	boolean add(int index,T data);
	/**
	 * 根据index移除元素
	 * @param index
	 * @return
	 */
	T remove(int index);
	/**
	 * 根据data移除元素
	 * @param data
	 * @return
	 */
	boolean remove(T data);
	/**
	 * 清空链表
	 */
	void clear();
	/**
	 * 是否包含data元素
	 * @param data
	 * @return
	 */
	boolean contains(T data);
	/**
	 * 根据值查询下标
	 * @param data
	 * @return
	 */
	int indexOf(T data);
	/**
	 * 根据data值查询最后一个出现在顺序表中下标
	 * @param data
	 * @return
	 */
	int lastIndexOf(T data);
	/**
	 * 输出格式
	 * @return
	 */
	String toString();
}

首先实现接口先实现接口MyList,从写接口中的方法,声明当前数组,当前数组大小及默认数组的长度大小为10。

	//当前数组
	private Object[] table;
	//当前数组长度
	private int size;
	//默认数组长度
	public final static int DEFULT_LENTHT=10;

此处声明两个构造方法,一个指定数据的长度,一个默认无参构造方法,而无参构造方法调指定默认数组长度大小为10;

	/**
	 *指定容器大小 
	 * @param capacity
	 */
	public MyArrayList(int capacity){
		this.table=new Object[capacity];
	}
	/**
	 * 无参构造方法,设置默认大小10
	 */
	public MyArrayList(){
		this(DEFULT_LENTHT);
	}

 来先看下几个简单的方法

判断是容器是否为空,只需判断当前数组的长度是否为0;

	/**
	 * 判断是否为空,判断当前容器大小是否为0
	 */
	@Override 
	public boolean isEmpty() {
 		return this.size==0;
	}

获得元素在容器的中的位置:由于容器本身是个数组,所有元素的位置也就是元素在数组中的下标,而确定元素的下标需要遍历所有元素从而找到元素的下标位置。

	/**
	 * 根据下标寻址,默认返回-1,表示该元素在容器中不存在
	 */
	@Override
	public int indexOf(T data) {
		if (data != null) {
			for (int i = 0; i < table.length; i++) {
				if (this.table[i].equals(data)) {
					return i;
				}
			}
		}
		return -1;
	}

判断元素最后一次出现的位置:与indexOf的方法相似,只是遍历的顺序会和indexOf相反(代码就省略了)

容器指定位置添加元素:因为数组的连续性,在未指定条件的情况,新加入顺序表的元素都会放入的数组的末端。当数组长度无法满足当前添加元素时,触发容器的动态扩容机制。

    @Override
    public void add(int index, T data) {
       //边界校验,插入元素失败
	    if (index < 0 || index > size()) {
		    return false;
	     }  
	    //动态扩容
        ensureCapacity(index);
	    // 插入新值
	    this.table[index] = data;
	    // 长度加1
	     size++;
	     return true;
    }
        /**
	 * 判断数据是否需要扩容,当容器大小与数组的大于等于数组大小时,
	 * 将原来的数据扩容到之前的2倍,并且搬移数组到新数组中
	 * @param index
	 */
	public void ensureCapacity(int index){
		if (size() == table.length) {
			//将当前数组赋值给临时数组
			Object[] temp = this.table;
			// 将原来的数组扩容到原来的两倍
			this.table = new Object[size() *2];
			// 把原来的数组赋值复制到新的数组中
			for (int i = 0; i < size(); i++) {
				this.table[i] = temp[i];
			}
		}
        //index后面的元素向后移动一位
		for (int j = size; j > index; j--) {
			this.table[j] = this.table[j-1];
		}
	}

指定位置添加元素时,默认在容器末尾添加元素:

	/**
	 * 在尾部插入元素
	 */
	@Override
	public boolean add(T data) {
		return this.add(size(),data);
	}
	

获取指定位置的元素:由于顺序表的底层是由数组实现,所以根据数组下标获取元素值,如果边界不合法返回null

	/**
	 * 获取元素(根据数组下标获取元素)
	 */
	@Override
	public T get(int index) {
		if(index> 0 && index<this.size){
			return (T) this.table[index];
		}
		return null;
	}

删除指定位置的元素:由于数组中的数据具有连续性,在删除数据组时,需要进行数据搬移才能保证数据的连续性,在大规模的数据搬移过程中,此时的性能消耗是不可忽略的。

	/**
	 * 删除指定位置的元素
	 */
	@Override
	public T remove(int index) {
		if(index > 0 && index<size){
			//记录删除的元素并返回
			T old=(T) this.table[index];
			//从被删除的元素开始,后面的元素依次上前移动一位
			for (int j = index; j < size-1; j++) {
				this.table[j]=this.table[j+1];
			}
			//将末尾元素设置为null
			this.table[size-1]=null;
			//顺序表长度-1
			--size;
			return old;
		}
		return null;
	}

删除指定元素:删除不为null的元素,首先找到在数组中找到指定元素的位置,然后将该元素后的元素进行搬移,将删除的元素赋值为null;

	public boolean remove(T data) {
		if (data != null) {
			for (int index = size; index > 0; index--) {
				if (data.equals(table[index])) {
					// 从被删除的元素开始,后面的元素依次上前移动一位
					for (int j = index; j < size - 1; j++) {
						this.table[j] = this.table[j - 1];
					}
					// 末尾的元素设置为null
					this.table[size - 1] = null;
					// 顺序表长度-1
					--size;
					return true;
				}

			}

		}
		return false;
	}

修改某个元素的值:校验边界合法性,根据下标获取该元素,用新元素替换旧元素

        /**
	 * 设置某个节点的值返回旧的节点
	 */
	@Override
	public T set(int index, T data) {
		if(index>0 && index < size  ){
			T olddata = (T) this.table[index];
			this.table[index]=data;
			return  olddata;
		}
		return null;
	}

 

以上就是顺序表最基本的增删改查方法,从上面的方法中,可以看出,顺序表结构在增删情况下由于要涉及数据搬移效率要明显低于读写,不过在已经数据较少同时对于性能不敏感的情况下,顺序表基本能满足需求,譬如ArrayList在我们实际系统中应用是比较广泛的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值