> 技术文档 > 数据结构的基本知识

数据结构的基本知识


一、集合框架

1、什么是集合框架

Java集合框架(Java Collection Framework),又被称为容器(container),是定义在java.util包下的一组接口(interfaces)和其实现类(classes).

主要表现为把多个元素(element)放在一个单元中,用于对这些元素进行快速、便捷的存储(store)、检索(retrieve)、管理(manipulate),即平时俗称的增删改查(CRUD)

类和接口总览

2、背后涉及的数据结构以及算法

2.1什么是数据结构

数据结构(Data Structure)是计算机存储、组织数据的方式,指互相之间存在一种或多种特定关系的数据元素的集合

2.2容器背后对应的数据结构

(1)Collection:一个接口,包含了大部分容器常用的一些方法

(2)List:一个接口,规范了ArrayList和LinkedList中要实现的方法

                        ArrayList:实现了List接口,底层为动态类型顺序表

                        LinkedList:实现了List接口,底层为双向链表

(3)Stack:底层是栈,栈是一种特殊的顺序表

(4)Queue:底层是队列,队列是一种特殊的顺序表

(5)Deque:是一个接口

(6)Set:集合,是一个接口,里面放置的是K模型

                        HashSet:底层为哈希桶,查询的时间复杂度为O(1)

                        TreeSet:底层为红黑树,查询的时间复杂度为O( logN ),关于key有序的

(7)Map:映射,里面存储的是K-V模型的键值对

HashMap:底层为哈希桶,查询时间复杂度为O(1)

TreeMap:底层为红黑树,查询的时间复杂度为O(logN),关于key有序

2.3什么是算法

算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

二、时间和空间复杂度

1、算法效率

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间

(在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。)

2、时间复杂度

2.1时间复杂度的概念

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个数学函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

2.2大O的渐进表示法

 // 请计算一下func1基本操作执行了多少次? void func1(int N){ int count = 0; for (int i = 0; i < N ; i++) { for (int j = 0; j < N ; j++) { count++; } }//N^2 for (int k = 0; k  0) { count++; }//10 System.out.println(count); }

也就是F(N) = N^2 + 2*N + 10

实际我们计算时间复杂度时,我们只需要算大概执行的次数,也就是大O的渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。

2.3推导大O阶方法

(1)用常数1取代运行时间中的所有加法常数(换常数)

(2)在修改后的运行次数函数中,只保留最高阶项(只保留最高相)

(3)如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。(系数变成1)

使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。 另外有些算法的时间复杂度存在最好、平均和最坏情况:

最坏情况:任意输入规模的最大运行次数(上界)(最慢)

平均情况:任意输入规模的期望运行次数

最好情况:任意输入规模的最小运行次数(下界)(最快)

2.4常见的时间复杂度计算举例

例一、

void func3(int N, int M) { int count = 0; for (int k = 0; k < M; k++) { count++;//M } for (int k = 0; k < N ; k++) { count++;//N } System.out.println(count); }

由于基本操作执行了N+M次,并且两数都是未知数,所以时间复杂度为O(N+M)

例二、

void bubbleSort(int[] array) { for (int end = array.length; end > 0; end--) { boolean sorted = true; for (int i = 1; i  array[i]) { Swap(array, i - 1, i); sorted = false; }//N*(N-1)/2 } if (sorted == true) { break; } } }

由于循环执行,第一次执行(N-1)次,最后一次执行了0次,共N次,求每次比前一次少一次,因此得到N*(N-1)/2,因此时间复杂度为O(N^2)

例三、

int binarySearch(int[] array, int value) { int begin = 0; int end = array.length - 1; while (begin <= end) { int mid = begin + ((end-begin) / 2); if (array[mid]  value) end = mid - 1; else return mid; } return -1; }

二分查找,一次是原来的一半可以得出(N/2)^O=1,计算可得时间复杂度为O(logN)(logN是以2为底,lgN是以10为底)

例四、

 long factorial(int N) { return N < 2 ? N : factorial(N-1) * N; }

阶乘递归是在比较N和2的大小关系进行选择,可以发现共递归了N次,所以时间复杂度为O(N)

例五、

 int fibonacci(int N) { return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2); }

可以发现,左侧最顶端(第一次)是(N-1),最后一次是1,也就可以得到近似的1+2^1+……+2^(N-1),也就是2^N-1,同理,右侧也可以计算出是2^(N-1)-1,因此时间复杂度为O(2^N)

我们常遇到的复杂度有:O(1) < O(logN) < O(N) < O(N*logN) < O(N^2)

3、空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。

例一、

void bubbleSort(int[] array) { for (int end = array.length; end > 0; end--) { boolean sorted = true; for (int i = 1; i  array[i]) { Swap(array, i - 1, i); sorted = false; } } if (sorted == true) { break; } }}

因为使用了常数个额外空间,所以空间复杂度为O(1)

例二、

 int[] fibonacci(int n) { long[] fibArray = new long[n + 1]; fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n ; i++) { fibArray[i] = fibArray[i - 1] + fibArray [i - 2]; } return fibArray; }

因为动态开辟了N个空间,空间复杂度为 O(N)

例三、

 long factorial(int N) { return N < 2 ? N : factorial(N-1)*N; }

因为递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间,因此空间复杂度为O(N)

三、包装类&简单认识泛类

1、包装类

在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。

1.1基本数据类型和对应的包装类

1.2装箱和拆箱

 int i = 10; // 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中 Integer ii = Integer.valueOf(i); Integer ij = new Integer(i); // 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中 int j = ii.intValue();

1.3自动装箱和自动拆箱

我们可以看到在使用过程中,装箱和拆箱带来不少的代码量,所以为了减少开发者的负担,java 提供了自动机制。

int i = 10;Integer ii = i;  // 自动装箱Integer ij = (Integer)i; // 自动装箱int j = ii;  // 自动拆箱int k = (int)ii; // 自动拆箱

2.泛型

实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?

思路:

(1)我们以前学过的数组,只能存放指定类型的元素,例如:int[] array = new int[10]; String[] strs = new String[10];

(2)所有类的父类,默认为Object类。数组是否可以创建为Object?

class MyArray { public Object[] array = new Object[10]; public Object getPos(int pos) { return this.array[pos]; } public void setVal(int pos,Object val) { this.array[pos] = val; } } public class TestDemo { public static void main(String[] args) { MyArray myArray = new MyArray(); myArray.setVal(0,10); myArray.setVal(1,\"hello\");//字符串也可以存放 String ret = myArray.getPos(1);//编译报错 System.out.println(ret); } }

问题:以上代码实现后发现

(1)任何类型数据都可以存放

(2)1号下标本身就是字符串,但是却编译报错。必须进行强制类型转换

虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译 器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

2.1语法

class 泛型类名称 { // 这里可以使用类型参数} class ClassName { }class 泛型类名称 extends 继承类/* 这里可以使用类型参数 */ { // 这里可以使用类型参数} class ClassName extends ParentClass { // 可以只使用部分类型参数}

上述代码进行改写如下:

class MyArray { public Object[] array = new Object[10]; public T getPos(int pos) { return (T)this.array[pos]; } public void setVal(int pos,T val) { this.array[pos] = val; } } public class TestDemo { public static void main(String[] args) { MyArray myArray = new MyArray();//1 myArray.setVal(0,10); myArray.setVal(1,12); int ret = myArray.getPos(1);//2 System.out.println(ret); myArray.setVal(2,\"bit\");//3 } }

代码解释:

(1)类名后的代表占位符,表示当前类是一个泛型类

(了解: 【规范】类型形参一般使用一个大写字母表示,常用的名称有:

E 表示 Element

K 表示 Key

V 表示 Value

N 表示 Number

T 表示 Type

S, U, V 等等 - 第二、第三、第四个类型)

(2)注释1处,类型后加入指定当前类型

(3)注释2处,不需要进行强制类型转换

(4)注释3处,代码编译报错,此时因为在注释2处指定类当前的类型,此时在注释3处,编译器会在存放元素的时候帮助我们进行类型检查。

3、泛型的使用

3.1语法

泛型类 变量名; // 定义一个泛型类引用new 泛型类(构造方法实参); // 实例化一个泛型类对象

例:

MyArray list = new MyArray();

#注:泛型只能接受类,所有的基本数据类型必须使用包装类!

3.2 类型推导(Type Inference)

当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

MyArray list = new MyArray(); // 可以推导出实例化需要的类型实参为 Integer

4、擦除机制

在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制。

Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。

编译完成以后,T被擦除为Object(编译期间用T进行类型的检查和转换)

5、泛型的上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

5.1语法

 class 泛型类名称 { ... }

例:

 public class MyArray { ... }

只接受 Number 的子类型作为 E 的类型实参

MyArray l1; // 正常,因为 Integer 是 Number 的子类型MyArray l2; // 编译错误,因为 String 不是 Number 的子类型

(当没有指定的边界时,可以认为E extends Object)

6、泛型方法

6.1定义语法

方法限定符  返回值类型 方法名称(形参列表) { ... }

例:

 public class Util { //静态的泛型方法 需要在static后用声明泛型类型参数 public static  void swap(E[] array, int i, int j) { E t = array[i]; array[i] = array[j]; array[j] = t; } }

四、List的介绍

1、什么是List

在集合框架中,List是一个接口,继承自Collection。

Collection也是一个接口(继承自Iterable),该接口中规范了后序容器中常用的一些方法,具体如下所示:

Iterable也是一个接口,表示实现该接口的类是可以逐个元素进行遍历的,具体如下:

站在数据结构的角度来看,List就是一个线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删改查以及变量等操作。

在这里我们先简单的认识一下List就可以了,后续内容会补充介绍,这里只需要简单的看一下有什么就可以了(List太多,有兴趣的话可以去自己看一下)