[数据结构] 复杂度和包装类和泛型
1.集合框架和数据结构
1.1 什么是数据结构
数据结构就是用来组织,存储或操作数据的,而数据结构有很多种,比如说:顺序表,单链表,树等。
1.2 什么是集合框架
Java中的集合框架也叫做容器,是java.util包下面的一组接口和其实现类。
集合框架相当于把数据结构用代码来具体实现,集合框架中的每一个类的底层逻辑对应一个数据结构,在集合框架中对数据进行处理。
集合框架本质上是将一组数据存在一起,进行增删改查的操作,这样操作比较高效。
1.3 主要的集合框架
下面是集合框架中的主要类和接口之间的关系,黄色是接口,粉色是类,绿色是抽象类。
Collection:接口,包含了大部分容器常用的一些方法。
List:接口,包含了ArrayList和LinkedList中要实现的方法。
ArrayList:实现了List接口,底层数据结构为动态类型顺序表。
LinkedList:实现了List接口,底层数据结构是双向链表。
Strack:底层是栈,栈是一种特殊的顺序表。
Queue:底层是队列,队列是一种特殊的顺序表。
Deque:是一个接口。
Set:集合,接口,里面是K模型。
HashSet:底层为哈希桶,查询的时间复杂度为O(1)。
TreeSet:底层为红黑树。
Map:映射,里面存储的是K-V模型的键值对。
HashMap:底层为哈希桶。
TreeMap:底层为红黑树。
2. 算法
算法就是定义一个运算过程,对已知的一些数据进行处理之后输出一些数据。
如何判断算法的好与坏呢?
这里用到了复杂度的概念来判断,而复杂度又分为时间复杂度和空间复杂度。
3. 时间复杂度
时间复杂度是用来表示代码的执行时间的,我们用一串代码里面的基本语句的执行次数与执行时间进行比较,两者之间成正比。我们就可以用代码的执行次数才表示代码的时间复杂度,这里我们用到大O的渐进表示法。
大O的渐进表示法规则:
- 代码的执行次数中存在加法运算的常数都用1来代替。
- 代码执行次数只保留最高项。
- 若存在最高项并且不为1,则将最高项的系数化为1。
举例:
Scanner sc = new Scanner(System.in);int n = sc.nextInt();for (int i = 0; i < 2 * n; i++) {for (int j = 0; j < n; j++) {System.out.println("111");}}for (int i = 0; i < n; i++) {System.out.println("222");}for(int i = 0; i < 9; i++) {System.out.println("333");}
上面代码的执行次数为: 2*n^2 + n + 9。
采用大O的渐进表示法则为:n^2
所以上述代码的时间复杂度为O(n^2)。
二分查找的时间复杂度:
public static int findNum(int[] arr, int n) {int left = 0;int right = arr.length;while(left <= right) {int cent = left + (right - left) / 2;if(arr[cent] > n) {right = cent;}else if(arr[cent] < n) {left = cent;}else {return cent;}}return -1;}
上面是二分查找的代码:
求的二分查找的时间复杂度为:O(n) = log(n);
递归的时间复杂度:
公式为:递归的次数 X 递归一次之后代码执行的次数。
求得的时间复杂度也符合大O的渐进表示法规则。
4. 空间复杂度
算法的空间复杂度是指一串代码在运行过程中临时占用存储空间的大小,空间复杂度计算的是
变量的个数。每个变量都对应一个空间。
5. 包装类
5.1 基本类型对应的包装类
基本类型不是继承于Object类的,为了能在范型代码中调用基本类型,Java给每个基本类型引进一个包装类型。
5.2 装箱和拆箱
装箱是指将基本数据类型转换为包装类型,装箱分为自动装箱和手动装箱。
//自动装箱int a = 10;Integer b = a;//手动装箱int i = 20;Integer j = Integer.valueOf(i);System.out.println(j);
自动装箱是编译器自动的调用包装类里面的valueOf方法。
手动装箱是我们自己调用valueOf方法。
拆箱是指将包装类型转换为基本数据类型,拆箱分为自动拆箱和手动拆箱。
//自动拆箱Integer c = 10;int d = c;//手动拆箱Integer e = 20;int f = e.intValue();System.out.println(f);
自动拆箱是编译器自己调用intValue方法。
手动拆箱是我们自己调用intValue方法。
装箱的底层代码:
Integer a = 100;Integer b = 100;System.out.println(a == b);Integer c = 200;Integer d = 200;System.out.println(c == d);
上面这串代码的输出结果是true 和 false;
为什么呢?
上面是装箱的过程,我们可以查看valueOf方法的源代码:
这里我们会发现在[low,high]范围里面的数字,会在cache数组里面查找,cache是缓存数组可以减少空间的占用,超过该范围的数字就要创建新的对象。
而low= -128, high = 127。
所以上面代码的输出结果为true和false。
6. 泛型
6.1 什么是泛型
泛型是指可以适用于多种类型,也就是实现了类型的参数化,可以使一个容器包含不同类型的数据。
6.2 泛型的产生
如果我们想要实现一个类,类中存在一个数组成员,数组里面可以包含多种类型的数据,并且可以根据方法返回指定下标对应的值。
不用泛型的思想实现的代码如下:
class MyArrays {Object[] arr = new Object[5];public void setArr(int a, Object b) {arr[a] = b;}public Object getArr(int a) {return arr[a];}
}
public class Test02 {public static void main(String[] args) {MyArrays myArrays = new MyArrays();myArrays.setArr(0,12);myArrays.setArr(1,"abc");String a = (String)myArrays.getArr(1);System.out.println(a);}
}
上面的代码实现了问题的需求,但是也存在一些问题:
数组里面可以同时存在不同类型的数据。
通过getArr方法得到的数据是字符串但是是Object类型,还需要自己强制类型转换为String类型才能接收,否则编译错误。
这两个问题便可以通过泛型来解决,泛型的作用是:自己可以指定容器里存储什么类型的数据,并且让编译器自己去检查类型是否匹配。把类型当作参数,传什么类型,容器里就是什么类型。
使用泛型修改后的代码:
class MyArrays<T> {Object[] arr = new Object[5];public void setArr(int a, T b) {arr[a] = b;}public T getArr(int a) {return (T)arr[a];}
}
public class Test02 {public static void main(String[] args) {MyArrays<Integer> myArrays = new MyArrays<Integer>();myArrays.setArr(1,12);int a = myArrays.getArr(1);System.out.println(a);MyArrays<String> myArrays1 = new MyArrays<>();myArrays1.setArr(1,"abc");String b = myArrays1.getArr(1);System.out.println(b);}
}
上面修改后的代码就可以实现自己指定类型就创建对应类型的数组,并且利用getArr方法得到的数组元素不用自己进行强制类型转换,由编译器进行强制类型转换。
上面的类名后面的<>是占位符,表示该类是一个泛型类。
<>里面的大写字母是自己定义的一个参数名字,只不过大家都用这个字母表示,提高代码的可阅读性。
E:Element
K:Key
V:Value
N:Number
T:Type
S,U,V对应: 第二、第三、第四个类型
类后面的<>里面只能是包装类型
可以省略创建对象类后面<>里面的内容:
MyArrays<Integer> myArrays = new MyArrays<>();
6.3 裸类型
裸类型是指没有加<>的类型;
MyArrays myArrays = new MyArrays();
这样的类型是为了兼容老版本的机制。
6.4 泛型如何进行编译
6.4.1 擦除机制
擦除机制是指泛型类型代码在编译时会将泛型的信息擦除,擦除后泛型类型会替换成Object类型或者指定类型。
擦除的过程:
将泛型类型参数替换成Object类型。
在合适的地方加入强制类型转换。
生成桥接方法保证代码的多态性。
擦除前:
class MyArrays<T> {Object[] arr = new Object[5];public void setArr(int a, T b) {arr[a] = b;}public T getArr(int a) {return (T)arr[a];}
}
擦除后:
class MyArrays {Object[] arr = new Object[5];public void setArr(int a, Object b) {arr[a] = b;}public Object getArr(int a) {return (Object)arr[a];}
}
6.4.2 桥接方法
泛型的擦拭可能会导致子类和父类的方法的重写机制失效。
为了体现Java的多态性,编译器会生成桥接的方法来解决问题。
擦拭前代码:
class MyArrays1<T> {T a;public void setArr(T a) {this.a = a;}
}
public class Test03 extends MyArrays1<String> {public void setArr(String a) {super.setArr(a);}
}
擦拭后代码:
class MyArrays1 {Object a;public void setArr(Object a) {this.a = a;}
}
public class Test03 extends MyArrays1 {public void setArr(String a) {super.setArr(a);}
}
擦拭后的代码中的Test03类里面的setArr方法跟上面的方法不构成重写(参数类型不同),因此我们需要利用桥接方法来实现,这时候编译器会生成一个桥接方法:
public void setArr(Object a) {setArr((String)a);}
通过这个桥接方法可以实现方法的重写。
6.5 泛型的上界
在定义一个泛型类时候,有时候需要对传入的数据进行约束,这里我们就可以设置一个上届。
6.5.1 语法
class 泛型类名<类型形参 extends 泛型类的上届(类/接口)> {}
6.5.2 示例
class Person<E extends Number> {}
形参E只要是Number的本身或者它的子类都可以。
class Person<E extends Comparable<E>> {}
这里的形参E必须实现Comparable接口。
6.6 泛型方法
6.6.1 语法
方法限定符 <类型形参> 返回值类型 方法名(方法参数) {
}
6.6.2 实例
public <E> void swap(E[] arr, int a, int b ) {E t = arr[a];arr[a] = arr[b];arr[b] = t;}
6.6.3 类型推导
在使用泛型方法时,正常情况下我们可以这样写:
public static void main(String[] args) {Integer[] arr = {1,2,3,4};Util.<Integer>swap(arr,1,2);}
但是我们也可以使用类型推导,省略调用方法时的<Integer>:
public static void main(String[] args) {Integer[] arr = {1,2,3,4};Util.swap(arr,1,2);}
6.7 通配符
这里我们把?称为统配符。
6.7.1 举例
class Message<T> {private T message;public T getMessage() {return message;}public void setMessage(T message) {this.message = message;}
}
public class Test02 {public static void main(String[] args) {Message<String> message = new Message<>();message.setMessage("你好,世界");fun(message);}public static void fun(Message<String> message) {System.out.println(message.getMessage());}
}
上面代码,我们可以把泛型类当作参数传递,但是这样传递只能使该方法使用一种指定的类型来调用泛型类,如何可以让泛型类参数也可以根据需求传入不同的类型,这里我们就用到了通配符,修改后的代码如下:
public static void fun(Message<?> message) {System.out.println(message.getMessage());}
这样就可以接收不同类型的参数了。
6.7.2 通配符的上界
通配符也存在上界,语法如下:
public static void fun(Message<? extends 类或者接口> message) {System.out.println(message.getMessage());}
这里传入的参数message类型只能是类或接口的本身或者是其子类。
6.7.3 通配符的下界
通配符也存在下界,这里用到关键字 super,语法如下:
public static void fun(Message<? super 类或者接口> message) {System.out.println(message.getMessage());}
这里传入的参数message类型只能是类或者接口的本身或者父类。