一起学数据结构:走进Java数据结构

290 阅读14分钟

目录:

1.什么是数据结构

数据是描述客观事务的数字、字符以及所有能输入计算机中并能接受的各种符号的统称。数据是信息的符号表示,是计算机程序的处理对象。

在计算机中表示一个事物的一组数据被称为一个数据元素(data element),数据元素是数据的基本单位,数据元素可以是一个不可分割的原子项,也可以是由多个数据项组成。

数据项(data item) 是数据元素中有独立含义的不可分割的最小标识单位。

关键字(key) 是数据元素中用于识别该元素的一个或多个数据项,能够唯一识别数据元素的关键字称为主关键字(primary key)

数据结构 是指数据元素之间存在的关系。一个数据结构是由n(n\geq 0)个数据元素组成的有限集合,数据元素之间具有某种特定的关系。数据结构概念包含三个方面:数据的逻辑结构,数据的存储结构和数据的操作。

1.1.数据的逻辑结构

数据的逻辑结构是指数据元素之间的逻辑关系,是一个数据元素的集合和定义在此集合上的若干关系来表示的。根据数据元素之间逻辑关系的不同数据特性,数据结构可分为三种:线性结构,树结构和图

1.1.1 线性结构

线性结构是最简单的数据结构,数据元素之间具有线性关系,即除第一个和最后一个元素外,每个元素有且仅有一个前驱元素和一个后继元素,第一个元素没有前驱元素,最后一个元素没有后继元素。

1.1.2 树结构

树结构是数据元素之间具有层次关系的一种非线性结构,树中数据元素通常称为结点。树结构的层次关系是值,根(最顶层)结点没有前驱结点(称为父母结点),除根之外的其他结点有且仅有一个父母结点,所有结点可有零到多个后继结点(孩子结点)。这种结构常见于windows的文件系统,电商的分类目录

1.1.3 图

图也是非线性结构,每个数据元素可有多个前驱元素和多个后继元素。如地铁交通图,全国火车网格图都具有图结构。

1.2数据的存储结构

数据的存储结构 是指数据元素及其关系在计算机中的存储表示或者实现。也称为物理结构。

数据的逻辑结构从逻辑关系角度观察数据,与数据的存储无关,是独立于计算机的。而数据的存结构是逻辑结构在计算机内存中的实现,是依赖于计算机的。

数据存储结构的基本形式有两种:顺序存储结构和链式存储结构;数据的存储方法有四种:顺序存储、链式存储、索引存储和散列存储。

  • 顺序存储结构是使用一组连续的内存单元依次存放数据元素,元素在内存中的物理存储次序与他的逻辑次序相同,即每个元素都与其前驱及后继元素的存储位置相邻。在程序中这种逻辑关系常体现在数组中
  • 链式存储结构是使用若干地址分散的存储单元存储数据元素,逻辑上相邻的数据元素在物理位置上不一定相邻,数据元素间的关系需要采用附加信息特别指定。通常,采用指针变量记载前驱或后继元素的存储地址,有数据域和地址域组成的一个结点表示一个数据元素,通常地址域把相互直接关联的结点链接起来,结点间的链接关系体现数据元素之间的逻辑关系

如果你觉得存储结构描述的不够详细,请参考 存储结构这篇专业文章

1.3数据操作

数据操作指对一种数据结构中的数据元素进行各种运算或处理。每种数据结构都有一组数据操作,其中包含以下基本操作:

数据操作定义在数据的逻辑结构上,对数据操作的实现依赖于数据的存储结构。

2.数据类型与抽象数据类型

2.1 数据类型

类型(type) 是具有相同逻辑意义的一组值的集合。**数据类型(data type)**是指一个类型和定义在这个类型上的操作集合。数据类型定义了数据的性质、取值范围以及对数据所能进行的各种操作。

程序中的每个数据都属于一种数据类型,决定了数据的类型也就决定了数据的性质以及对数据进行的运算和操作,同时数据也受到类型的保护,确保对数据不能进行非法操作。

Java中数据类型有byte, short, int, long, char, float, double, boolean,还有构造函数(引用类型),数组,类,接口等。

数据类型与数据结构两个概念的侧重点不同。数据类型研究的是每种数据所具有的特性,以及对这种特性的数据能够进行哪些操作;数据结构研究的是数据元素之间具有的相互关系,数据结构与数据元素的数据类型无关,也不随数据元素值的变化而改变。

2.2 抽象数据类型

抽象数据类型(Abstract Data Type,ADT) 是指一个数学模型以及定义在该模型上的一组操作。

2.2.1 数据抽象

抽象数据类型和数据类型本质上是一个概念,它们都表现数据的抽象特性。数据抽象 是指“定义和实现分离”,即将一个类型上的数据及操作的逻辑含义与具体实现分离。程序设计语言提供的数据类型是抽象的,仅描述数据的特性和对数据操作的语法规则,并没有说明这些数据类型是如何实现的。 在Java中定义了List、Set、Map等集合类型,在实际开发过程中使用这些类型时,只需要考虑对数据执行什么操作,而不必考虑怎么实现这些操作。

数据抽象是研究复杂对象的基本方法,也是一种信息隐蔽技术,从复杂对象中抽象出本质特征,忽略次要细节,使实现细节相对于使用者不可见。抽象层次越高,其软件复用程度也越高。抽象数据类型是实现软件模块化设计思想的重要手段。一个抽象数据类型是描述一种特定功能的基本模块,由各种基本模块可组织和构造起来的一个大型软件系统。

2.2.2 抽象数据类型的声明

抽象数据类型的规范描述包括ADT名称、数据描述和操作描述。 如:

public interface List<E> extends Collection<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    boolean remove(Object o);
    boolean removeAll(Collection<?> c);
    boolean equals(Object o);
    void add(int index, E element);
}

与使用数据类型描述数据特性一样,通常使用抽象数据类型描述数据结构,将线性表,树,图等数据结构分别定义为各种抽象数据类型,一种抽象数据类型描述一种数据结构的逻辑特性和操作,与该数据结构在计算机内的存储及实现无关。

如果你觉得存储结构描述的不够详细,请参考 抽象数据类型这篇专业文章

3.算法

3.1 什么是算法

图灵奖获得者著名计算科学家D.Knuth对算法做过一个学术界广泛接受的描述定义:一个算法是一个有穷规则的集合,其规则确定一个解决某一种特定类型问题的操作序列。 算法的规则必须满足以下5个特点:

  • 有穷性 算法的操作步骤为有限个,且每步都能在有限时间内完成。
  • 确定性 对于各种情况下的操作,在算法中都有确切的的规定,使算法的执行者或阅读者都能明确其含义及如何执行。并且在任何条件下,算法都只有一条执行路径。
  • 可行性 算法原则上能够精确地运行,而且做有限次运算后即可完成。
  • 有输入 算法有零个或多个输入数据。输入数据是算法的加工对象,既可以由算法指定,也可以在算法执行过程中通过输入得到。
  • 有输出 算法有一个或多个输出数据。输出数据是一组与输入有确定关系的量值,是算法进行信息加工后得到的结果。

3.2 算法设计目标

算法设计应该满足以下5个目标:

  • 正确性 满足应用问题的需求
  • 健壮性 即使输入数据不合适,算法也能做出适当处理,不会导致不可控结果
  • 高时间效率 算法执行时间越短,时间效率越高
  • 高空间效率 算法执行时占用的存储空间越少,空间效率越高。
  • 可读性 简洁明了,易于理解

3.3 算法描述

算法是对问题求解过程的描述,精确地指出怎样从给定的输入信息得到要求的输出信息,其中操作步骤的语义明确,操作序列的长度有限。 例伪码展示一个查找算法:

类型 search(类型 key){
    e = 数据序列的第一个元素;
    while(数据序列未结束 && e != key){
        e = 数据序列的下一个元素;
        返回查找到的元素或查找不成功标记;
    }
}

3.4 算法与数据结构

开发人员都明白程序=数据结构+算法。而算法是建立在数据结构之上,对数据结构的操作需要用算法来操作。例如:List有插入、删除、遍历、查找、排序等操作。算法设计依赖于数据的逻辑结构,算法实现依赖于数据的存储结构。例如:数组的插入和删除操作,采用顺序存储结构,由于数据元素是相邻存储的,所以插入前和删除后都必须移动一些元素。

实现一种抽象数据类型,需要选择合适的存储结构,使得以下两方面的综合性能最佳:数据操作所花费的时间最短,占用的存储空间最少。

4.算法分析

4.1 时间代价分析

算法的时间代价是指算法执行时所花费的CPU时间量,是算法中涉及的存、取、转移、加、减等各种基本运算的执行时间之和,与参加的数据量有关。算法的时间效率是指算法的执行时间随问题规模的增长而增长的趋势,通常采用时间复杂度来度量。 当问题的规模以某种单位从1增加到n时,解决这个问题的算法在执行时所耗费的时间也以某种从1增加到T(n),就称此算法的时间复杂度未T(n)。当n增大时,T(n)也随之增大。采用算法的渐进分析中的大O表示法作为算法时间复杂度的渐进度量值。 大O表示法是指,当且仅当存在正整数c和n_0,使得T(n) \leq cf(n) 对所有的 n\geq n_0 成立时,称该算法的时间增长率与f(n) 的增长率相同,记为T(n)=O(f(n))

若算法的执行时间是常数级,不依赖于数据量n的大小,则时间复杂度为O(1);若算法的执行时间是n的线性关系,则时间复杂度为O(n)。同理,对数级、平方级、立方级、指数级的时间复杂度分别为O(log_2n)O(n^2)O(n^3)O(2^n)。 这些函数按数量级递增排列具有下列关系:O(1)<O(log_2n)<O(n)<O(nlog_2n)<O(n^2)<O(n^3)<O(2^n)

时间复杂度O(f(n)) 随数据量n变化情况的比较如下表:

时间复杂度 n=8(即2^3) n=10 n=100 n=1000
O(1) 1 1 1 1
O(log_2n) 3 3.322 6.644 9.966
O(n) 8 10 100 1000
O(nlog_2n) 24 33.22 664.4 9966
O(n^2) 64 100 10000 10^6

算法中时间复杂度估算公式:算法的执行时间 = \sum_{i}基本操作(i)的执行次数  \times  基本操作(i)的执行时间

由于算法的时间复杂度表示算法执行时间的增长率而非绝对时间,因此可以忽略一些次要的因素,算法的执行时间绝大部分花在循环和递归上。设基本操作的执行时间为常量级O(1),则算法的执行时间是基本操作执行次数之和,以此作为估算算法时间复杂度的依据,可表示算法本身的时间效率。

每个算法渐进时间复杂度中的f(n),可由统计程序步数得到,与程序结构有关。循环语句的时间代价一般可用下面三条原则进行分析:

  • 一个循环的时间代价=循环次数x每次执行的简单语句数目。
  • 多个并列循环的时间代价=每个循环的时间代价总和。
  • 多层嵌套循环的时间代价=每层循环的时间代价之和。

常见代码中的时间复杂度分析:

a.一个简单语句的时间复杂度为O(1)

    int a = 0;

b.执行n次的循环语句,时间复杂度为O(n)

int count = 0;
for(int i=0;i<10;i++){
    count ++;
}

c.时间复杂度为O(log_2n) 的循环语句:

int count = 0;
for(int i=0;i<10;i*=2){
    count ++;
}

d.时间复杂度为O(n) 的二重循环语句:

int count = 0;
for(int i=0;i<10;i*=2){
    for(int j=0;j<=i;j++){
        count ++;
    }
}

e.时间复杂度为*O(n^2)* 的循环语句:

int count = 0;
for(int i=0;i<10;i++){
    for(int j=0;j<=i;j++){
        count ++;
    }
}

4.2 空间代价分析

算法的空间代价是指算法执行时所占的存储空间量。

执行一个算法所需要的存储空间包括三部分:输入数据占用的存储空间、程序指令占用的存储空间、辅助变量占用的存储空间。其中,输入数据和程序指令所占用的存储空间与算法无关,因此,辅助变量占用的存储空间就成为度量算法空间代价的依据。

当问题的规模以某种单位从1增大n时,解决这个问题的算法在执行时所占用的存储空间也以某种单位从1增大到S(n),则称此算法的空间复杂度(space complexity)为S(n)。当n增大时,S(n) 也随之增大。 空间复杂度用大O表示法记为 S(n)=O(f(n)),表示该算法的空间增长率与 f(n) 的增长率相同。

比如,交换两个变量i、j算法,除了程序指令和i、j本身占用的存储空间之外,为了实现交换操作,还必须声明一个临时变量tmp,这个tmp变量所占用的一个存储单元就是交换变量算法的空间复杂度O(1)