数据结构(1)/ 时间、空间复杂度与泛型

目录

一、复杂度

Ⅰ、时间复杂度与空间复杂度

Ⅱ、大O的渐进表示法

二、泛型

Ⅰ、为什么会有泛型

Ⅱ、泛型语法

Ⅲ、裸类型(了解即可)

Ⅳ、泛型如何编译?

擦除机制

知识点:为什么不能创建泛型数组

创建泛型数组常规操作

Ⅴ、泛型的边界 

Ⅵ、泛型方法

Ⅶ、通配符(?)

①、通配符解决什么问题

②、 通配符的上界

③、通配符的下界 

④、包装类 


前言 

本章节是在IntelliJ IDEA集成开发环境测试的结果,内容多适用于学习Java方向数据结构的学者


一、复杂度

Ⅰ、时间复杂度与空间复杂度

时间复杂度:算法中基本代码的运行次数

空间复杂度:为实现一个算法而创建的空间次数

例如:

void func(int n){
    int count = 0;
    //时间复杂度为n
    for(int i = 0 ; i < n ; i++){
        count++;  
    }
    //时间复杂度为n^2
    for(int i = 0; i < 2 * n;i++){
        count++;
    }

    int M = 10;
    //时间复杂度为10
    while(M > 0){
        M--;
        count++;
    }
}

Ⅱ、大O的渐进表示法

原理:复杂度相加,保留最高次项数,舍去常数项。

例如:由上代码复杂度如下

func( n ) =  n^2 + 2*n + 10

 保留最高次项数,舍去常数项,就是这个方法的时间复杂度。记作O(n^2)

空间复杂度同理

原因:

n = 10时:           func = 130

n = 100时:         func = 10210

n = 10000时:     func = 100020010

n = 1000000时: func = 1000002000010

可以发现当 n 越大时 ,结果越接近最高次项数(n^2)。

二、泛型

Ⅰ、为什么会有泛型

我们在学完java SE之后只能使用具体的类型(要么是基本类型 int char等等,要么是自定义类),如果需要编写可以应用于多种类型的代码,那么之前学的语法就会有很大的束缚。所以在JDK1.5之后引入了新的泛型语法,通俗讲,泛型:就是适用于多种类型,你想存什么类型数据或者自定义类都能实现

由以下实列证明束缚在哪:

 问题:

①Object数组任何类型数据都可以存放,但是具体存放的数据类型我们并不知道,后续需要调用时就显得很麻烦,所以直接用Object数组来存放数据不安全。

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

首先我们知道Object为所有基本类型的父类,那么我们可以创建一个Object数组来存放各种基本类型,但是当需要使用这些数据时,我们不知道object中存放的是那种数据类型,即使知道也需要进行强制转换,这显得有点不太灵活,简而言之就是被束缚了。

虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能持有一种类型。而不是同时持有这么多类型。

所以泛型的主要目的:指定当前容器要持有什么类型的对象,让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

Ⅱ、泛型语法

class  类型名称<指定类型形参列表>

 例:class Main<Integer>  、 class MyClass<Integer,Character>、class Text<Main<Integer>>

 由以上代码改编如下

class MyArray<T> {
    public T[] array = (T[])new Object[10];//①
    //获得指定位置的元素
    public T getPos(int pos) {
        return this.array[pos];
    }
    //设置指定位置的元素
    public void setVal(int pos,T val) {
        this.array[pos] = val;
    }
}
public class Main {
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray();//②
        myArray.setVal(0,10);
        myArray.setVal(1,12);
        int ret = myArray.getPos(1);
        System.out.println(ret);
        myArray.setVal(2,"bit");//③编译报错
    }
}

问题:

①在代码①处为什么不这样写 public T[] array = new T[10];

解释:java中不能new一个泛型数组,知识点:为什么不能创建泛型数组

② 泛型的尖括号当中只能使用引用类型(Integer),不能用简单类型(int)

③编译报错,编译器会自动帮我们检测存入的类型是否与指定类型相同,不相同就会报错,相比不使用泛型语法,这样更不容易出错。

④以上我们强制转换了Object数组,这种操作并不是很好,因为这样做你只是骗过了编译器,常规操作如下创建泛型数组常规操作

 由此,以上MyArray中可以存放我们指定的数据类型

课外知识:

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

E  表示  Element

K  表示  Key

V  表示  Value
N  表示  Number
T  表示  Type
S、U、V等---- 第二、第三、第四个类型

 Ⅲ、裸类型(了解即可)

直接上例子:

class MyArray<T> {
    public T[] array = (T[])new Object[10];

    public T getPos(int pos) {
        return this.array[pos];
    }
    public void setVal(int pos,T val) {
        this.array[pos] = val;
    }
}
public class Main {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.setVal(0,12);
        myArray.setVal(1,"hello");
    }
}

以上编译不报错,运行也没问题。

可以看到我们没使用尖括号指定泛型类,这样的类型就叫做裸类型。但是我们不要自己去使用裸类型,裸类型只是为了兼容老版本的API而保留的机制。以下擦除机制,会提到编译器是如何使用泛型的。

Ⅳ、泛型如何编译?

①、擦除机制

查看字节码文件方式:

①通过命令javap -c

②通过idea附加插件(jclasslib,需要手动下载) 

 图一中array原本是T[]类型的,但是编译之后变成了Object类型。

图二中setVal方法中形参val原本是T类型,编译之后却变成了Object类型。

观察所有T出现的地方,我们都发现编译之后T类型都变成了Object类型,那么我们就把这种编译机制叫做擦除机制,由此可以说明在编译后,编译器生成的字节码文件不存在泛型这个概念。

注:字节码文件中 

 更详细的泛型擦除机制介绍————<泛型擦除机制>

知识点:为什么不能创建泛型数组

由以上擦除机制我们知道如果创建了一个泛型数组,那么在编译之后就变成了Object数组,而你就可以存放任意类型数据,那么你就可以乱来,随便存放数据,所以编译器为了规范这种操作,就不让我们创建泛型数组。从另一方面来讲你只能用Object[]来接收Object数组,因为Object是所有类的父类,而你并不知道里面存放的数据类型,需要取值的时候很不安全。 

创建泛型数组常规操作:

我们原本时这样创建泛型数组的:

public T[] array = (T[])new Object[10];

由擦除机制我们知道array在编译之后变成了Object类,而如果你想在外部得到这个数组,那么就有以下问题:

public class MyArray<T> {
    public T[] array = (T[])new Object[10];
    public T getPos(int pos) {
        return this.array[pos];
    }
    public void setVal(int pos,T val) {
        this.array[pos] = val;
    }
    public T[] getArray() {
        return array;
    }
    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<>();
        Integer[] strings = myArray1.getArray();
    }
}

以上代码编译器并不会报错,看着逻辑确实没问题,但是运行时出现了异常:

 意思时Object类型不能由Integer类型接收

也就是说你自己创建了泛型数组其实就是Object数组,这样还是没解决不安全的问题。那么我们还可以通过反射,创建指定类型的数组。

import java.lang.reflect.Array;

class MyArray<T> {
    public T[] array;

    public MyArray() {
    }
    //构造时指定类型和容量
    public MyArray(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);
    }

    public T getPos(int pos) {
        return this.array[pos];
    }

    public void setVal(int pos, T val) {
        this.array[pos] = val;
    }

    public T[] getArray() {
        return array;
    }

    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<>(Integer.class, 10);
        Integer[] integers = myArray1.getArray();
    }

}

Ⅴ、泛型的边界 

 我们在定义泛型时,如果不指定泛型的上界,那么默认泛型的上界为Object,如

public class MyArray<T>

public class MyArray<T extends Object>

这里extends不是继承的意思,表示的是T的上界为Object。

这两种定义方式都相同。那么为什么有这种机制呢?

在编译之后编译器会将泛型擦除成泛型的上界,比如T擦除成Object。这样你需要指定类型时只能指定上界(Object)及上界的子类,指定其他的类会报错。下面举个例子来快速理解这样做的好处。

例:如果我们想指定一种类型数组,找到这种类型数组的最大值 ,应该如何求解呢

或许我们第一时间会想到遍历数组用等于号比大小,但是这是错误的,我们指定的类型是引用类型,不是基本类型,本身不能比较大小,这时候就可以使用泛型上界擦除机制:

public class MyArray<T extends Comparable<T>> {
    public T finMax(T[] array){
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(array[i].compareTo(max) > 0){
                max = array[i];
            }
        }
        return max;
    }
}

比较大小的时候max与array被擦除成了Comparable类型,而Comparable中有比较方法,当然T需要实现Comparable接口

 Ⅵ、泛型方法

顾名思义:含有泛型的方法,但是这个方法可以是静态方法。因为静态静态方法不依赖于对象,所以可以使用泛型方法实现。

格式:方法限定符 返回值类型 方法名称(形参列表)

 例如以上代码可以改编成:

 class Text {
    public static<T extends Comparable<T>> T finMax(T[] array){
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(array[i].compareTo(max) > 0){
                max = array[i];
            }
        }
        return max;
    }
}
public class MyArray {
    public static void main(String[] args) {
        Integer[] arr = {1,34,123,4235,546,2};
        Integer max = Text.finMax(arr);
        System.out.println(max);
    }
}

 Ⅶ、通配符(?)

①、通配符解决什么问题

通配符是用来解决泛型无法协变的问题,协变指的是如果Water是Object的子类,那么List<Water>也应该是List<Object>的子类。但是泛型不支持这样的父子类关系。

 下面是一个通配符的例子(代码有错误):

class Message<T> {
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}
public class TextDemo {
    public static void main(String[] args) {
        Message<String> message = new Message() ;
        Message<Integer> message1 = new Message<>();
        message.setMessage("hello");
        message1.setMessage(111);
        fun1(message);
        fun1(message1);
        fun2(message);
        fun2(message1);
    }
    public static void fun1(Message<String> temp){
        System.out.println(temp.getMessage());
    }
    public static void fun2(Message<?> temp){
        System.out.println(temp.getMessage());
    }
}

 可以看到fun1指定了只能传入String类型的泛型,而fun2中使用了通配符可以传通配符范围内任意的数据类型。

②、 通配符的上界

格式:public static void fun1(Message<?extends Water> temp)

表示在fun1方法传参时,泛型的类型只能是Water及Water的子类。

③、通配符的下界 

格式:public static void fun1(Message<?super Water> temp)

表示在fun1方法传参时,泛型的类型只能是Water及Water的父类 

④、包装类 

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

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

装箱与拆箱:

装箱:将基本数据类型变为包装类类型

Integer a = 1;//自动装箱、隐式装箱
Character b = new Character('f');//手动装箱、显式装箱

拆箱:将包装类类型变为基本数据类型

Integer integer = 10;//自动装箱
int val = integer;//自动拆箱
int val1 = integer.intValue();//手动拆箱
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寒夕君哎

动力+99(*•̀ᴗ•́*)و

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值