数据结构(1)线性表的动态数组实现ArrayList

1.何为线性表

首先说什么是线性表

线性表的定义:线性表中数据元素之间的关系是一对一的关系,即除了第一个元素没有前驱,最后一个元素没有后继之外,其余元素既有唯一前驱和唯一后继,如下图所示

搞懂什么是线性表之后,我们再考虑一个问题,线性表能干啥?

手机上的通讯录是不是符合一个线性表的定义?

刺激战场中背包内容是不是符合一个线性表的定义?

学校中的成绩单是不是符合一个线性表的定义?

是的,对于上述三个场景都符合线性表的定义,只不过主要的区别在于所存储的数据元素不同罢了

这里大家注意,所谓的数据元素不只是整数int,小数double,布尔boolean和字符串String等等简单的数据类型

数据元素,是由多个数据项组成的集合体,大家可以定义一个类对这些数据项进行封装,那么线性表中只需要存储由这个类所创建出来的若干个对象即可

比如手机通讯录里的联系人数据元素,由头像、姓名、电话三个数据项组成

刺激战场中背包的物品数据元素,由图片、名称、说明、属性四个数据项组成

成绩单中学生分数的数据元素,由姓名、语文分数、数学分数、英语分数、总分、平均分六个数据项组成

到这里大家就能够明白,线性表的主要作用就是存储类似上述场景中的数据

除了存储之外,还应该具有增加数据,删除数据,查寻数据,修改数据这四个基本的操作,对吧~

 

2.逻辑结构和物理结构

到目前为止,我们一直在讨论线性表能够做什么,但是如何让计算机能够认识且存储线性表就是我们现在要考虑的问题

上述讨论的线性表其实是我们臆想出来的,还没有真实的存储在于计算机内存中,我们把它称之为线性表的逻辑结构

什么是逻辑结构,就是只知道他的定义,但不知如何将其进行实际的实现

在计算机领域中,对于线性表的实现方式主要有两种,称之为线性表的物理结构

  • 动态数组:用编程语言中自带的数组去实现线性表,如图所示

  • 动态链表:定义节点数据类型,将元素存入节点,然后将节点进行串联实现线性表,如图所示

我们会发现,如果用动态数组实现线性表,那么元素在数组中存储的话是地址连续的,因为数组中的存储空间是连续的

如果用动态链表实现线性表的话,那么元素的地址是随机的,因为节点对象创建时的地址是由系统底层决定的且随机的,所以为了保持线性表的性质,每一个节点除了存储数据信息外,还需要存储其下一个节点的地址

无论是动态数组实现线性表也好,还是动态链表实现也罢,它们对线性表的操作无非还是增删改查,所以我们可以先对两者的具体实现定义统一的操作规范,即定义线性表的接口,如下代码所示

创建List.java写入代码

//让List线性表支持泛型,E表示任意的数据类型
public interface List<E> {
    public int getSize();        	//获取线性表中元素的个数
    public boolean isEmpty();           //判断线性表是否为空表
    public void add(int index,E e);    	//在线性表中指定角标index处插入元素e
    public void addFirst(E e);	        //在线性表中第一个位置插入元素e
    public void addLast(E e);	        //在线性表中最后一个位置插入元素e
    public E get(int index);		//获取线性表中指定角标index处的元素
    public E getFirst();		//获取线性表中第一个元素
    public E getLast();		        //获取线性表中最后一个元素
    public void set(int index,E e);	//修改线性表中指定角标index处的元素为新元素e
    public boolean contains(E e);	//判断线性表中是否包含指定元素e
    public int find(E e);		//获取线性表中元素e从头到尾第一次出现的位置
    public E remove(int index);	        //删除并返回线性表中指定角标index处的元素
    public E removeFirst();		//删除并返回线性表中第一个元素
    public E removeLast();		//删除并返回线性表中最后一个元素
    public void removeElement(E e);	//删除线性表中指定元素e
    public void clear();		//清空线性表
}

 

3.Java中数组的特点

创建好List接口之后,我们接下来就来讨论如何用动态数组实现线性表

首先我们来回顾一下Java中数组的特点,下面代码是Java中创建一维数组的三种常用方式:

int[] arr1=new int[100];
int[] arr2=new int[]{1,2,3,4,5};
int[] arr3={1,2,3,4,5};
  • 第一种,创建名为arr1的一维整型数组,指定长度为100,且元素默认为0
  • 第二种,创建名为arr2的一维整型数组,指定元素为[1,2,3,4,5],且长度为5
  • 第三种,和第二种同理

除了创建方式之外,数组还有以下几个特点:

  • 数组提供角标访问元素
  • 数组长度一旦确定则不可更改
  • 数组只有唯一的属性length表示数组的长度

那么数组的本质是什么的?数组本质就是一组空间大小相同连续的存储结构,如图所示

已知第1个空间的地址为0x100,且每个空间大小都是4byte,那么第n个空间的大小是多少呢?

A_n=A_1+(n-1)\ast d

大家会发现,这是一个等差公式,对的!此时A1=0x100,d=4,所以一个公式直接算出第n个位置元素的地址

所以基于这一特点,我们可以用O(1)的时间去查找一个元素

注意此处 n 为元素在数组中的位置,而 n-1 表示元素在数组中的角标

int[] arr={1,2,3,4,5};
arr[3];    //表示角标3的元素,即就是位置4的元素 A4=A1+(4-1)*4
arr[4];    //表示角标4的元素,即就是位置5的元素 A5=A1+(5-1)*4
arr[5];    //表示角标5的元素,即就是位置6的元素,但是该元素不存在,所以报错

OK,数组的本质说完了,再来看看数组的操作

我们之前学过和数组相关的操作,基本上都是将数组当做参数传入到一个函数当中

//查找数组中的最大值
public int findMax(int[] arr){...}
//对数组进行排序
public void sort(int[] arr){...}
//交换角标i,j元素的位置
public void swap(int[] arr,int i,int j){...}

这样子的话,我们是将数组这个数据和函数这个操作进行分离的,并没有很好的体现面向对象的思想

如果能够将数据和操作进行封装成一个类MyArray,形成如下的操作方式,岂不是更好?

class MyArray{
	private int[] arr;
	public MyArray(){
		arr=new int[10];
	}
	public void add(int e){...}
	public int findMax(){...}
	public void sort(){...}
	public void swap(int i,int j){...}
}
MyArray arr=new MyArray();
arr.add(1);
arr.add(2);
arr.add(3);
arr.sort();
arr.swap(0,1);
arr.findMax();

那么,我们就可以把对数组的所有操作都定义在这个MyArray类中,将来调用起来会很方便的

所以,将数组和其相关操作进行类封装的过程称之为动态数组

 

4.线性表的动态数组实现

哈哈,结合线性表的定义和动态数组的概念,我们就可以真正开始写线性表的动态数组实现方式了

4.1 创建ArrayList类并实现List接口

public class ArrayList<E> implements List<E>{

}

至于List中的方法,稍后我们在一个个实现,别急

4.2 成员变量

对于线性表的动态数组实现而言,如果只封装一个数组数据够用吗?

如上图所示,虽然数组data长度为10,但是只有5个元素存入到数组当中

10很好表示,data.length即可

5怎么表示呢?所以我们还需要维护一个记录线性表中有效元素个数的变量 size

如下图所示

小技巧提示,size不仅可以表示有效元素的个数,也可以表示在末尾添加元素时新元素的位置

public class ArrayList<E> implements List<E>{
	private E[] data;	//作为容器存储元素 data.length为最大容量 
	private int size;	//当前线性表中元素的个数-有效长度
        
}

4.3 构造函数

可以让调用者创建一个指定容量大小的线性表,也可以创建一个默认容量大小的线性表

public class ArrayList<E> implements List<E>{
	private E[] data;	//作为容器存储元素 data.length为最大容量 
	private int size;	//当前线性表中元素的个数-有效长度
	private static final int DEFAULT_CAPACITY=10;//默认容量为10
	//默认构造函数中创建一个默认容量的线性表
	public ArrayList(){	
		this(DEFAULT_CAPACITY);
	}
	//构造函数中创建一个容量为capacity的线性表
	public ArrayList(int capacity){	
		if(capacity<0){
			capacity=DEFAULT_CAPACITY;
		}
		this.data=(E[]) new Object[capacity];
		this.size=0;
	}
}

增删功能在后头,一会再说哈,先把几个简单的问题解决了~困难的放后头

4.4 getSize()函数

获取线性表中元素的个数

直接返回size即可

	public int getSize() {
		return this.size;
	}

4.5 isEmpty()函数

判断线性表是否为空表

直接判断size==0的结果即可

	public boolean isEmpty() {
		return this.size==0;
	}

4.6 get()函数

获取线性表中指定角标index处的元素

直接返回数组[index],注意角标越界问题

	public E get(int index) {
		if(index<0||index>=data.length){
			throw new IllegalArgumentException("角标不存在!");
		}
		return data[index];
	}

4.7 getFirst()函数

获取线性表中第一个元素

复用get()函数

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

4.8 getLast()函数

获取线性表中最后一个元素

复用get()函数

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

4.9 set函数

修改线性表中指定角标index处的元素为新元素e

	public void set(int index, E e) {
		if(index<0||index>=size){
			throw new IllegalArgumentException("角标不存在!");
		}
		data[index]=e;
	}

4.10 find()函数

获取线性表中元素e从头到尾第一次出现的位置

	public int find(E e) {
		for(int i=0;i<size;i++){
			if(data[i].equals(e)){
				return i;
			}
		}
		return -1;
	}

4.11 contains()函数

判断线性表中是否包含指定元素e

	public boolean contains(E e) {
		return find(e)!=-1;
	}

4.12 clear()函数

清空线性表

简单粗暴

	public void clear() {
		this.data=(E[]) new Object[DEFAULT_CAPACITY];
		this.size=0;
	}

4.13 getCapacity()函数

获取线性表的最大容量

	public int getCapacity(){
		return data.length;
	}

4.14 swap()函数

交换顺序表中指定角标i,j的两个元素

	public void swap(int i,int j){
		if(i<0||i>=size||j<0||j>=size){
			throw new IllegalArgumentException("角标非法!");
		}
		E temp=data[i];
		data[i]=data[j];
		data[j]=temp;
	}

好了,现在开始复杂的部分了,增删!其实也不难哈

4.15 add()函数

在线性表中指定角标index处插入元素e

先别急着写代码,分析一下,添加元素主要分为三种情况

  • 在表头插入:index=0
  • 在表中插入:index∈(0,size)任意
  • 在表尾插入:index=size

如果在表头插入元素,原先已存在的元素就得挨个后移,别忘了size++

思路:另 i 从 size 开始,i 到角标 0 结束,每次将 i-1 的元素赋予 i ,完毕后将新元素加入角标0 如下图所示

如果在表中插入元素,基本思路和上述一直,插入位置 index 之后的元素就得挨个后移,别忘了size++

思路:另 i 从 size 开始,i 到角标 index 结束,每次将 i-1 的元素赋予 i ,完毕后将新元素加入角标 index 如下图所示

如果在表尾插入元素,直接把元素放入size位置,size++即可,此处无图,自己想

其实三者插入方式都是一个意思的,完全可以按照一个思路写,只不过表尾插入不需要循环,可以直接插入即可,代码如下

	public void add(int index, E e) {
		if(index<0||index>size){
			throw new IllegalArgumentException("角标非法!");
		}
		if(size==data.length){
			resize(data.length*2);
		}
		for(int i=size;i>index;i--){
			data[i]=data[i-1];
		}
		data[index]=e;
		size++;
	}
	private void resize(int newLen) {
		E[] newData=(E[]) new Object[newLen];
		for(int i=0;i<Math.min(data.length,newData.length);i++){
			newData[i]=data[i];
		}
		data=newData;
	}

有同学会发现,这里怎么还多了个resize函数呢?

其实这个函数是为了实现数组的自动扩容和缩容功能的

如果元素填满了数组( size==data.length),此时就要需要考虑数组该扩容了,一般扩容为原先的2倍,如图所示

思路:创建一个比原先数组大2倍的新数组,然后将老数组中的值挨个放入到新数组中,最后新数组替代老数组即可

恩,在任意位置添加元素讲完了

4.16 addFirst()函数

在线性表中第一个位置插入元素e

复用add()即可

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

4.17 addLast()函数

在线性表中最后一个位置插入元素e

复用add()即可

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

4.18 remove()函数

删除并返回线性表中指定角标index处的元素

哈哈,此处的删除也分为删除表头,删除表中,和删除表尾

此处我就不给出演示图了,希望大家自己可以动手画一画

删除表头思路:先取出要删除的元素,然后另 i 从 0 开始,到size-2结束,将 i+1 的元素赋予 i  即可,最后 size--

删除表中思路:先取出要删除的元素,然后另 i 从 index 开始,到size-2结束,将 i+1 的元素赋予 i  即可,最后 size--

删除表尾思路:直接size--即可

此处的重点在于,删除元素之后,可能有缩容的情况,什么时候缩容呢,当 size==data.length/4 时缩容

为什么是除以4,而不是除以2呢?

如果是 size==data.length/2 时缩容,大家可以考虑一个问题,如果新加元素之后满了,扩容;接着删除元素之后,size到达length的一半,缩容;这样子的话我们的线性表是不是太“勤快”了些呢?是的,这样子的话会影响效率,所以删除元素之后,就算size到达length的一半,别急着缩,让它再删几个,删到足够少的时候再缩容,为时也不晚,一般设置在1/4处即可;这样子就避免了缩的太勤的问题。

还有就是,如果已达到默认容量的话,则不需要再缩了

	public E remove(int index) {
		if(index<0||index>=size){
			throw new IllegalArgumentException("角标非法!");
		}
		E res=data[index];
		
		for(int i=index;i<size-1;i++){
			data[i]=data[i+1];
		}
		size--;
		if(size==data.length/4&&data.length>DEFAULT_CAPACITY){
			resize(data.length/2);
		}
		return res;
	}

4.19 removeFirst()函数

删除并返回线性表中第一个元素

复用remove()即可

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

4.20 removeLast()函数

删除并返回线性表中最后一个元素

复用remove()即可

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

4.21 removeElement()函数

删除线性表中指定元素e

先找,再删

	public void removeElement(E e) {
		int index=find(e);
		if(index!=-1){
			remove(index);
		}
	}

 

欧克,好啦,到这里线性表的动态数组实现方式讲完了,大家再接再厉,附上完整代码

ArrayList.java

/**
@Author Teacher_HENG
*/
//动态数组实现的线性表->顺序表
public class ArrayList<E> implements List<E>{
	private E[] data;	//作为容器存储元素 data.length为最大容量 
	private int size;	//当前线性表中元素的个数-有效长度
	private static final int DEFAULT_CAPACITY=10;//默认容量为10
	//默认构造函数中创建一个默认容量的线性表
	public ArrayList(){	
		this(DEFAULT_CAPACITY);
	}
	//构造函数中创建一个容量为capacity的线性表
	public ArrayList(int capacity){	
		if(capacity<0){
			capacity=DEFAULT_CAPACITY;
		}
		this.data=(E[]) new Object[capacity];
		this.size=0;
	}
        //将传入数组封装成动态数组
	public ArrayList(E[] arr){
		data=(E[]) new Object[arr.length];
		for(int i=0;i<data.length;i++){
			data[i]=arr[i];
		}
		size=arr.length;
	}
	public void add(int index, E e) {
		if(index<0||index>size){
			throw new IllegalArgumentException("角标非法!");
		}
		if(size==data.length){
			resize(data.length*2);
		}
		for(int i=size;i>index;i--){
			data[i]=data[i-1];
		}
		data[index]=e;
		size++;
	}
	public void addFirst(E e) {
		add(0,e);
	}
	public void addLast(E e) {
		add(size,e);
	}
	public E remove(int index) {
		if(index<0||index>=size){
			throw new IllegalArgumentException("角标非法!");
		}
		E res=data[index];
		
		for(int i=index;i<size-1;i++){
			data[i]=data[i+1];
		}
		size--;
		if(size==data.length/4&&data.length>DEFAULT_CAPACITY){
			resize(data.length/2);
		}
		return res;
	}
	public E removeFirst() {
		return remove(0);
	}
	public E removeLast() {
		return remove(size-1);
	}
	@Override
	public void removeElement(E e) {
		int index=find(e);
		if(index!=-1){
			remove(index);
		}
	}
	private void resize(int newLen) {
		E[] newData=(E[]) new Object[newLen];
		for(int i=0;i<Math.min(data.length,newData.length);i++){
			newData[i]=data[i];
		}
		data=newData;
	}
	public int getSize() {
		return this.size;
	}
	public boolean isEmpty() {
		return this.size==0;
	}
	public E get(int index) {
		if(index<0||index>=data.length){
			throw new IllegalArgumentException("角标不存在!");
		}
		return data[index];
	}
	public E getFirst() {
		return get(0);
	}
	public E getLast(){
		return get(size-1);
	}
	public void set(int index, E e) {
		if(index<0||index>=size){
			throw new IllegalArgumentException("角标不存在!");
		}
		data[index]=e;
	}
	public int find(E e) {
		for(int i=0;i<size;i++){
			if(data[i].equals(e)){
				return i;
			}
		}
		return -1;
	}
	public boolean contains(E e) {
		return find(e)!=-1;
	}
	public int getCapacity(){
		return data.length;
	}
	public void clear() {
		this.data=(E[]) new Object[DEFAULT_CAPACITY];
		this.size=0;
	}
	public void swap(int i,int j){
		if(i<0||i>=size||j<0||j>=size){
			throw new IllegalArgumentException("角标非法!");
		}
		E temp=data[i];
		data[i]=data[j];
		data[j]=temp;
	}
}

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值