数据结构专题——线性表之顺序表及其 Java 实现

本贴最后更新于 2370 天前,其中的信息可能已经时移世异

上一篇博客中,博主为大家介绍了关于数据结构的三个基本概念:数据结构数据类型抽象数据类型

在本篇博客中,博主将为大家介绍本系列中第一个出场的数据结构——线性表。顾名思义,线性表就是一种具有像线一样性质的表。比如在操场上站成一列的学生们,总有一个打头,一个结尾,中间的任何一个学生都知道自己前一名同学是谁和后一名同学是谁,就像有一根线将他们串了起来一样。

像这种从逻辑上看,具有零个或多个数据元素的有限序列我们就称之为线性表(List).

让我们简单来解读一下线性表的概念。首先它是一个序列,也就是说在线性表中的元素之间是有顺序的,当这个线性表不为空时,除了第一个元素只有后继元素,最后一个元素只有前驱元素之外,其余的元素都有且只有一个前驱和一个后继。线性表中同样允许没有元素,这时我们称之为空表。

还记得我们在上一篇博客中提到的,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,这种结构可以是逻辑结构或者是物理结构。上面对线性表的定义,明显是从逻辑结构上进行的。

那么如果从不同的存储形式(物理结构)上来看,在内存中顺序存储的线性表我们称之为顺序表,在内存中链式存储的线性表我们称之为链表。

也就是说,顺序表与链表也都是数据结构,它们是从物理存储结构角度上定义的线性表,可以看作是抽象的线性表在计算机编程语言中具体的实现。

接下来我们就一起来定义一下,线性表的抽象数据类型(ADT)

	ADT List{
		Data: 
		线性表中的数据对象集合为{a1,a2,a3... an},所有元素的数据类型必须都是DataType
		除了第一个元素a1只有后继元素,最后一个元素an只有前驱元素之外,其余的元素都有且只有一个前驱和一个后继。
		
		Operation:
		int size();                        //返回线性表中元素的个数
		boolean isEmpty();                 //检查线性表是否为空
		boolean contains(Object o);        //检查线性表是否包含某个元素
		boolean add(E e);                  //在线性表的结尾插入一个元素
		void add(int  index, E element);   //在线性表指定位置插入一个元素
		boolean remove(Object o);          //在线性表中删除指定元素
		E remove(int index);               //删除线性表上指定位置的元素
		void clear();                      //清空整个线性表
		E get(int index);                  //返回指定位置上的元素
		E set(int index, E element);       //替换指定位置上的元素
		
	}

在 Java 中,我们可以将线性表的抽象数据类型 List 抽象为一个接口类 MyList(为了区分 java.util.List),并将上面我们归纳出的操作放在接口类中。

	interface MyList<E>{
		int size();                        //返回线性表中元素的个数
		boolean isEmpty();                 //检查线性表是否为空
		boolean contains(Object o);        //检查线性表是否包含某个元素
		boolean add(E e);                  //在线性表的结尾插入一个元素
		void add(int  index, E element);   //在线性表指定位置插入一个元素
		boolean remove(Object o);          //在线性表中删除指定元素
		E remove(int index);               //删除线性表上指定位置的元素
		void clear();                      //清空整个线性表
		E get(int index);                  //返回指定位置上的元素
		E set(int index, E element);       //替换指定位置上的元素
	}

接下来博主就带着大家一起用 Java 实现一个在内存中顺序存储的线性表(也叫顺序表)。要想在内存中顺序存储,我们必须将元素放在数组当中,因此我们的顺序表就起名叫做 MyArrayList。

  • 首先我们来写这个数据结构的私有成员,一个能装泛型 E 的数组,size 表示列表中元素的数目,以及一个默认的初识数组大小
public class MyArrayList<E> implements MyList<E>{

    //在声明MyArrayList时,如不指明大小,则初始大小为10
    private static final int DEFAULT_CAPACITY = 10;
    private E[] contents;
    private int size = 0;

    //两种构造函数,允许用户创建指定大小或者默认大小的线性表
    public MyArrayList(){
        init(DEFAULT_CAPACITY);
    }
    public MyArrayList(int initCapacity){
        init(initCapacity);
    }
    //私有化方法init帮助构造函数来初始化数组contents
    private void init(int capacity){
        //注意不能建立泛型数组,因此我们强行转换一个Object数组
        contents = (E[]) new Object[capacity];
    }
  • 接下来我们实现获取元素个数和查看是否为空的方法,很简单单行代码即可实现
	@Override
	public int size() {
		// TODO Auto-generated method stub
		return this.size;
	}

	@Override
	public boolean isEmpty() {
		// TODO Auto-generated method stub
		return size()==0;
	}
  • 然后是两种不同的插入方法
	@Override
	public boolean add(E e) {
		// TODO Auto-generated method stub
		//在插入元素之前,检查数组是否有足够的空间放置新的元素,若没有,则对数组进行扩容
		ensureCapacity();
		contents[size++] = e;
		return true;
	}

	@SuppressWarnings("unchecked")
	private void ensureCapacity() {
		// TODO Auto-generated method stub
		if(size()>=contents.length) {
			//此处新数组的容量是旧数组的2倍加1,你可以自己选择扩容的多少
			E[] newContents = (E[]) new Object[2*contents.length+1];
			//将就数组中的值全部拷于新数组中
			System.arraycopy(contents, 0, newContents, 0, size());
			//再让contents指向新的数组
			contents = newContents;
		}
	}
	@Override
	public void add(int index, E element) {
		// TODO Auto-generated method stub
		//一旦要插入元素的位置为负或大于目前的元素数量就抛出异常
        //此处允许index等于size,相当于在列表末尾插入元素
		if(index<0 || index>size())
			throw new ArrayIndexOutOfBoundsException();
		//在插入元素之前,检查数组是否有足够的空间放置新的元素,若没有,则对数组进行扩容
		ensureCapacity();
		//将数组中从index位置开始的size-index个元素复制到数组从index+1开始的位置去
		//相当于将数组中index位置之后的元素顺次向后移动一位
		System.arraycopy(contents, index, contents, index+1, size() - index);
		//将要插入的元素放置到index位置上去
		contents[index] = element;
		this.size++;
	}
	/*
	 * 等价与上面的add(int index, E element)方法
	 * 
	 * System.arraycopy(contents, index, contents, index+1, size() - index);
	 * 等价于:
	 * for(int i=size(); i>index; i--){ 
			contents[i] = contents[i-1];
	   }
	 * 
	  @Override
	  public void add(int index, E element) {
		  // TODO Auto-generated method stub
		  //一旦要插入元素的位置为负或大于目前的元素数量就抛出异常
		  //此处允许index等于size,相当于在列表末尾插入元素
		  if(index<0 || index>size())
			  throw new ArrayIndexOutOfBoundsException();
		  //在插入元素之前,检查数组是否有足够的空间放置新的元素,若没有,则对数组进行扩容
		  ensureCapacity();
		  for(int i=size(); i>index; i--) 
			  contents[i] = contents[i-1];
		  //将要插入的元素放置到index位置上去
		  contents[index] = element;
		  this.size++;
	  }*/
  • 紧接着是两个不同的删除方法
	@Override
	public boolean remove(Object o) {
		// TODO Auto-generated method stub
		if(o == null) {
			for(int i=0; i<size(); i++) {
				if(contents[i] == null) {
					//如果找到元素为null,就使用私有方法fastRemove将该位置元素删除
					fastRemove(i);
					return true;
				}
			}
			return false;
		}else {
			for(int i=0; i<size();i++) {
				if(contents[i].equals(o)) {
					//如果找到元素为o,就使用私有方法fastRemove将该位置元素删除
					fastRemove(i);
					return true;
				}
			}
			return false;
		}
	}
	private void fastRemove(int index) {
		// TODO Auto-generated method stub
		//需要从后向前移动的元素数目
        int numMoved = size() - index -1;
        if(numMoved > 0){
            //将数组i+1位置开始的numMoved个元素移动到数组i的位置
            //相当于将i位置上的元素删除,并将后面的元素向前移一位
            System.arraycopy(contents,index+1,contents,index,numMoved);
        }
        //将元素数目减一并释放原来最后一位的内存
        contents[--size] = null;
	}
	
	/*
	 * 等价与上面的fastRemove(int index)方法
	 * 
	 * System.arraycopy(contents,index+1,contents,index,numMoved);
	 * 等价于:
	 * for(int i=index; i<size()-1; i++){
			contents[i] = contents[i+1];
	   }
	 * 
	  private void fastRemove(int index) {
		  // TODO Auto-generated method stub
		  for(int i=index; i<size()-1; i++)
			  contents[i] = contents[i+1];
		  //将元素数目减一并释放原来最后一位的内存
		  contents[--size] = null;
	  }*/
	
	@Override
	public E remove(int index) {
		// TODO Auto-generated method stub
		//一旦要插入元素的位置为负或大于目前的元素数量就抛出异常
        //此处不允许index等于size
		checkIndexValidation(index);
		E element = contents[index];
		fastRemove(index);
		return element;
	}
	
	private void checkIndexValidation(int index) {
		if(index<0 || index>=size())
			throw new ArrayIndexOutOfBoundsException();
	}
  • 之后是获取,修改,查看是否为空,查看是否包含,以及清空整个列表这几个简单的方法了:
	@Override
	public void clear() {
		// TODO Auto-generated method stub
		for(int i=0; i<size(); i++) {
			contents[i] = null;
		}
		size = 0;
	}

	@Override
	public E get(int index) {
		// TODO Auto-generated method stub
		checkIndexValidation(index);
		return contents[index];
	}

	@Override
	public E set(int index, E element) {
		// TODO Auto-generated method stub
		checkIndexValidation(index);
		E old = contents[index];
		contents[index] = element;
		return old;
	}
	@Override
	public boolean contains(Object o) {
		// TODO Auto-generated method stub
		if(o == null) {
			for(int i=0 ;i<size(); i++) {
				if(contents[i] == null)
					return true;
			}
			return false;
		}else {
			for(int i=0 ;i<size(); i++) {
				if(o.equals(contents[i]))
					return true;
			}
			return false;
		}
	}

在完成了自己写的一个 MyArrayList 之后,我们分析一下这种数据结构的特点。很明显,对于基于数组实现的线性表来说

我们要读取或修改某一个位置的元素只花费常数时间 O(1)
我们要增加或删除某一个元素最坏情况下要花费线性时间 O(n),因为涉及到移动其它的元素

因此,当我们在遇到频繁访问列表中元素时,ArrayList 是一个很好的选择;但当我们需要频繁添加、删除列表中元素,尤其是在表的前端进行增删操作时,ArrayList 就不是一个特别好的选择了。

到这里我们自己实现的 MyArrayList 就完成了,是不是感觉自己写数据结构也没有那么难呢?但是我们这里完成的这个只是一个很简单的数据结构,也没有实现 Itrable 等接口。

要想真正提高巩固自己的 Java 水平,我们需要在自己写完之后去对比源码中 ArrayList 的实现。看看尽管功能都是实现的差不多,为什么大牛写的代码就很优雅、效率高。不断地比较、吸收、学习,我们才能进步下去。

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • RESTful

    一种软件架构设计风格而不是标准,提供了一组设计原则和约束条件,主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

    30 引用 • 114 回帖
  • 京东

    京东是中国最大的自营式电商企业,2015 年第一季度在中国自营式 B2C 电商市场的占有率为 56.3%。2014 年 5 月,京东在美国纳斯达克证券交易所正式挂牌上市(股票代码:JD),是中国第一个成功赴美上市的大型综合型电商平台,与腾讯、百度等中国互联网巨头共同跻身全球前十大互联网公司排行榜。

    14 引用 • 102 回帖 • 404 关注
  • jsoup

    jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 jQuery 的操作方法来取出和操作数据。

    6 引用 • 1 回帖 • 462 关注
  • GitHub

    GitHub 于 2008 年上线,目前,除了 Git 代码仓库托管及基本的 Web 管理界面以外,还提供了订阅、讨论组、文本渲染、在线文件编辑器、协作图谱(报表)、代码片段分享(Gist)等功能。正因为这些功能所提供的便利,又经过长期的积累,GitHub 的用户活跃度很高,在开源世界里享有深远的声望,并形成了社交化编程文化(Social Coding)。

    207 引用 • 2031 回帖
  • 微软

    微软是一家美国跨国科技公司,也是世界 PC 软件开发的先导,由比尔·盖茨与保罗·艾伦创办于 1975 年,公司总部设立在华盛顿州的雷德蒙德(Redmond,邻近西雅图)。以研发、制造、授权和提供广泛的电脑软件服务业务为主。

    8 引用 • 44 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3168 引用 • 8207 回帖
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 89 关注
  • Sillot

    Sillot (汐洛)孵化自思源笔记,致力于服务智慧新彖乄,具有彖乄驱动、极致优雅、开发者友好的特点
    Github 地址:https://github.com/Hi-Windom/Sillot

    15 引用 • 6 回帖 • 28 关注
  • 智能合约

    智能合约(Smart contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。智能合约概念于 1994 年由 Nick Szabo 首次提出。

    1 引用 • 11 回帖 • 7 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    284 引用 • 4481 回帖 • 654 关注
  • 游戏

    沉迷游戏伤身,强撸灰飞烟灭。

    169 引用 • 799 回帖
  • ngrok

    ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。

    7 引用 • 63 回帖 • 598 关注
  • Jenkins

    Jenkins 是一套开源的持续集成工具。它提供了非常丰富的插件,让构建、部署、自动化集成项目变得简单易用。

    51 引用 • 37 回帖
  • Maven

    Maven 是基于项目对象模型(POM)、通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具。

    185 引用 • 318 回帖 • 344 关注
  • 开源

    Open Source, Open Mind, Open Sight, Open Future!

    396 引用 • 3416 回帖
  • JSON

    JSON (JavaScript Object Notation)是一种轻量级的数据交换格式。易于人类阅读和编写。同时也易于机器解析和生成。

    51 引用 • 190 回帖
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 345 关注
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    215 引用 • 462 回帖
  • 友情链接

    确认过眼神后的灵魂连接,站在链在!

    24 引用 • 373 回帖
  • 安全

    安全永远都不是一个小问题。

    189 引用 • 813 回帖
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    25 引用 • 191 回帖 • 21 关注
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 443 关注
  • Swagger

    Swagger 是一款非常流行的 API 开发工具,它遵循 OpenAPI Specification(这是一种通用的、和编程语言无关的 API 描述规范)。Swagger 贯穿整个 API 生命周期,如 API 的设计、编写文档、测试和部署。

    26 引用 • 35 回帖 • 13 关注
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    129 引用 • 793 回帖 • 2 关注
  • RYMCU

    RYMCU 致力于打造一个即严谨又活泼、专业又不失有趣,为数百万人服务的开源嵌入式知识学习交流平台。

    4 引用 • 6 回帖 • 40 关注
  • Sandbox

    如果帖子标签含有 Sandbox ,则该帖子会被视为“测试帖”,主要用于测试社区功能,排查 bug 等,该标签下内容不定期进行清理。

    370 引用 • 1215 回帖 • 582 关注
  • Vditor

    Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript、Vue、React 和 Angular。

    313 引用 • 1667 回帖 • 1 关注