java数据结构_1

目录

前言:如何学好数据结构:

前置知识:

开始学习:

一 初识集合框架

1.什么是集合框架???

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

2.1什么是数据结构:

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

2.3什么是算法:

2.4如何学好数据结构和算法呢

二 时间和空间复杂度

引入:如何衡量一个算法的好坏呢?

1.算法效率

2.时间复杂度

2.1时间复杂度的概念:

2.2用大O渐进法来表示时间复杂度

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

3.空间复杂度

3.1空间复杂度的概念

3.2空间复杂度计算举例

三 包装类&简单认识泛型

1 包装类

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

1.2 装箱和拆箱

2.什么是泛型

3.引出泛型

3.1泛型语法

4.泛型类的使用

4.1语法

4.2类型推导

5泛型是如何编译的???

5.1擦除机制

6.泛型的上界

6.1语法

6.2使用


前言:如何学好数据结构:

1.多画图

2.多思考

3.多写代码

前置知识:

1.泛型

2.包装类(包装类):

Java中集合类的背后就是数据结构,其类型又有很多,有些书把这些类也会叫集合框架,若要学习数据结构,就应先学习其对应的集合类,要学习集合类,就要先学习其对应的集合框架。

3.为什么会又这么多中数据结构:

要先了解什么是数据结构:数据结构 = 数据 + 结构 ==》描述或者组织一些数据

描述或者组织一些数据的方式不同因而会有那么多的数据结构,每一个集合类,其描述和组织数据的方式都是不同的,在实际应用中,都会有不同的应用场景。

开始学习:

一 初识集合框架

1.什么是集合框架???

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

        将多个元素放在一个单元中,对这些元素进行快速,便捷的存储,检索,管理,即增删改查CRUD。

        下图是类和接口的一个总览图,这里先放在此处留个眼熟,后续会慢慢了解。

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

2.1什么是数据结构:

        计算机存储,组织数据的方式,(是按顺序放,还是无序随便放,是按正序还是按照逆序,能不能重复放相同的数据等等等等),指数据元素之间相互存在一种或者多种特定的关系的集合。

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(log2N),关于Key有序

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

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

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

2.3什么是算法:

定义一个良好的计算过程,取一个或一组值作为输入,产生一个或者一组作为输出。简而言之,算法就是一系列的计算过程,将输入转变为输出的计算过程。(类比于高中数学题,有些题目普通学生可能需要推理半天过程,而有些学霸,一个二级公式咣咣咣三行就得到了结果,学霸的二级公式,和普通学生十几行的推理过程,都是算法)

2.4如何学好数据结构和算法呢

1.死磕就行,奶奶滴,劳资当年数学题不会就算了,这b代码,实在不会,我就先全部死磕背会...

2.多画图,多动脑思考,把过程用纸和笔自己推演一下,慢慢的就会很清晰

3.刷题 + 复盘,牛客网力扣 and so on

二 时间和空间复杂度

引入:如何衡量一个算法的好坏呢?

        在Javaee的初阶语法学习中,大家都已大致掌握了敲代码,那我们这种初学java敲出的代码和几十年程序猿敲的代码好坏如何评判呢?

        例如:打印一个金字塔

        我们直接

System.out.println("*");

System.out.println("**");

System.out.println("***");

System.out.println("*****");

.....

        而大佬直接一个双层循环直接薄纱我们,虽然结果一样,但过程却明显我们的比较拉跨....

1.算法效率

        算法效率分为两种:时间效率(时间复杂度) 空间效率(空间复杂度)。时间复杂度衡量的是算法的运行速度(同样大家跑的是五千米,我三分配跑完,小明七分配跑完,要举办运动会,班主任当然会选我去参加比赛) ,空间复杂度衡量的是一个算法所需要的额外空间。

(早期计算机发展时候,存储容量都很小,大家对其存储的大小容量还是十分在乎,如今动辄手机都512G,电脑固态硬盘1T,2T的容量使得现在大家不是很注意空间复杂度了)

2.时间复杂度

2.1时间复杂度的概念:

        算法的时间复杂度是一个数学函数,定量了描述了算法的运行时间。(一个算法的运行时间,按理讲,应该是具体在某台机器上跑起来才可能知道,这种方法十分麻烦)所以,算法中的基本操作(在该算法中,被执行最多的操作)的执行次数,就是时间复杂度。

2.2用大O渐进法来表示时间复杂度

        1.用常数1取代运行时间中所有的加法常数。

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

        3.如果最高阶的项存在且不是1,则去除这个最高阶的系数

这样三步之后,得到的结果就是大O阶(大O渐进法去掉了那些对结果影响不大的项)(在实际中一般关注算法的最坏运行情况)

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

【实例1】

// 计算func2的时间复杂度?
void func2(int N) {
    int count = 0;
    for (int k = 0; k < 2 * N ; k++) {
        count++;
    }
    int M = 10;
    while ((M--) > 0) {
        count++;
    }
    System.out.println(count);
}

解析:基本操作实行了2N + 10次,根据大O渐进法,时间复杂度为O(N) (如果不理解,请严格按照2.2中的三步走)

【实例2】

// 计算func3的时间复杂度?
void func3(int N, int M) {
    int count = 0;
    for (int k = 0; k < M; k++) {
        count++;    
    }
    for (int k = 0; k < N ; k++) {
        count++;
    }
    System.out.println(count);
}

解析:基本操作执行了M + N次,时间复杂度为O(M + N)

【实例3】

// 计算func4的时间复杂度?
void func4(int N) {
    int count = 0;
    for (int k = 0; k < 100; k++) {
        count++;
    }
    System.out.println(count);
}

解析:基本操作执行了100次,时间复杂度为O(1) (这里为什么是1而不是100?请看2.2三步走)

【实例4】

// 计算bubbleSort的时间复杂度?
void bubbleSort(int[] array) {
    for (int end = array.length; end > 0; end--) {
        boolean sorted = true;
        for (int i = 1; i < end; i++) {
            if (array[i - 1] > array[i]) {
                Swap(array, i - 1, i);
                sorted = false;
            }
        }
        
        if (sorted == true) {
            break;
        }
    }
}

解析:

这是我自己动手大概写了一下过程,希望大家可以看懂。

最好的情况,就是全部都是有序的,1,2,3,4,5,只需要完成for(int i = 1;i < end; i++)中的循环,N次,发现sorted 仍然等于false,跳出循环,只需要进行N次。

【实例5】

// 计算binarySearch的时间复杂度?
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)
            begin = mid + 1;
        else if (array[mid] > value)
            end = mid - 1;
        else
            return mid;
    }
    return -1;
}

解析:二分法

对时间复杂度的计算,不仅要看代码,还要结合其算法的思想

【实例6】

// 计算阶乘递归factorial的时间复杂度?
long factorial(int N) {
    return N < 2 ? N : factorial(N-1) * N;
}

解析:递归算法,递归的复杂度 = 递归的次数 * 每次递归执行的次数

【实例7】

// 计算斐波那契递归fibonacci的时间复杂度?
int fibonacci(int N) {
    return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}

解析:斐波那契递归

总结:

O(1) < O(log2N) < O(N) < O(log2N * N) < O(N^2)

3.空间复杂度

3.1空间复杂度的概念

        一个算法在运行过程中临时占用存储空间大小的量度。注意:空间复杂度并不是程序占用了多少bytes的空间,而算的是变量的个数,也用大O渐进法表示。

3.2空间复杂度计算举例

因空间复杂度并不经常关注,此处仅举例三个,供大家了解。

【实例1】

// 计算bubbleSort的空间复杂度?
void bubbleSort(int[] array) {
    for (int end = array.length; end > 0; end--) {
        boolean sorted = true;
        for (int i = 1; i < end; i++) {
            if (array[i - 1] > array[i]) {
                Swap(array, i - 1, i);
                sorted = false;
            }
        }
    if (sorted == true) {
        break;
    }
}

解析:仅仅是在每次进行交换的时候,使用了两个常数的额外空间,所以空间复杂度O(1)

【实例2】

// 计算fibonacci的空间复杂度?
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次就开辟了N个空间,所以空间复杂度为O(N)

【实例3】

// 计算阶乘递归Factorial的空间复杂度?
long factorial(int N) {
    return N < 2 ? N : factorial(N-1)*N;
}

解析:递归了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。O(N)

三 包装类&简单认识泛型

1 包装类

        Java中,基本类型(int,byte,short,long,float...)并不是类,不继承自Object,为了在泛型代码中可以支持对应的基本类型,Java给每个基本类型都对应了一个包装类型

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

整理发现:除int --> Integer, char --> Character,其他基本数据类型的包装类均为首字母大写

1.2 装箱和拆箱

        装箱:把一个基本数据类型 转化为 包装类型 的过程

        分为自动装箱 和 显示装箱

int a = 10;
Integer b = a; //自动装箱
System.out.println(b);

Integer c = Integer.valueOf(a); //显示装箱
System.out.println(c);

        拆箱:将包装类型 转化为 基本数据类型 的过程

Integer a = new Integer(10); //这段代码在JDK9版本之后会报错
Integer a = new Integer.valueOf(10); //JDK9版本之后推荐使用这行代码
int b = a;//自动拆箱

int c = a.intValue();//显示拆箱
int d = a.doubleValue();//显示拆箱,可以拆箱为指定的基本数据类型

System.out.println(b);
System.out.println(c);
System.out.println(d);

可以打开字节码文件看一下装箱和拆箱的底层实现

打开方法如下(IDE集成开发环境)

会跳转到下面的目录中

此处返回上一层目录,并点击进入out目录下

打开对应的工程目录,在目录处输入cmd回车

输入javap -c 空格 + 全路径

注意:这里的全路径是类的位置,如果在testDemo包下的类Test,路径是:testDemo.Test

可以发现,虽然我代码直接是Integer b = a;但底层实现仍然是Integer.valueOf方法。java为了减少开发者的负担,提供了自动装箱和拆箱的机制。

【阿里巴巴曾经的一道面试题】

请问我下面代码输出结果是什么?

运行结果:

为什么呢???让我们一点一点庖丁解牛式搞明白其中的奥秘。

首先,我们肯定知道,这一定使用了装箱的方法,将127这个int类型自动装箱成了Integer包装类。通过上文,我们知道,装箱的底层肯定是通过valueOf()方法进行的,可以在IDE中找到valueOf方法的实现(把鼠标移到valueOf处,按住ctrl键,单击valueOf即可跳转到其方法体处)

观察可知,valueOf方法,如果输入的int类型i是在IntegerCache.low 和 IntegerCache.high范围内,就会返回一个cache数组内的元素,否则就会返回一个new Integer。

那IntegerCache.low和IntegerCache.high范围是什么呢?

鼠标放在IntegerCache.low处显示其值为-128

按住Ctrl键后点high,进行跳转,可以发现high = h,h = 127

这样我们就知道了,如果输入的数字在-128 -- 127之间,就会返回cache数组内的元素

-128 -- 127 共有256个元素

如果输入的 i 等于-128,[i + (-Integer.low)]  下标为0,即是在cache数组下标为0的位置存放数据-128

如果输入的i等于127,[i + (-Integer.low)]  下标为255,即是在cache数组下标为255的位置存放数据127

如果输入的数字在这个范围内,则会直接在数组中拿出值,然后return回一个Integer包装类

如果不在这个范围内,则会return new Integer(i),new一个新的Integer,并返回。

知道了valueOf的实现逻辑,此时再回头看面试题,第一组是127,第二组是128

127在范围内,直接从数组中返回,所以a == b --> true

128不在范围内,new了新的Integer返回,所以 c == d --> false

怎么样,你理解了吗???

2.什么是泛型

一般的类和方法,只能使用其固定具体的类型,要么是基本类型,要么是自定义类型。如果要编写可以应用于多种类型的代码,这么刻板的限制会对代码的束缚很大。

泛型是在Jdk1.5中引入的语法,泛型:就是适用于许多许多类型。从代码角度讲,就是把类型实现了参数化

3.引出泛型

实现一个类,类中有一个类型为Object的数组成员,使得数组中可以存放任何类型的数据,同样也可以根据成员方法返回数组中的某个下标的值。

以前数组的定义,都是固定的类型,例如String[], int[], double[],但是,可以想到,所有的类的父类都是Object,数组是否可以创建为Object???

实现如下:

的确可以实现,但发现有两个问题

        1.任何类型的数据都可以存放(下面会解释)

        2.下标为0的数据本身就是字符串,但如果不强制类型转化,会出现这样的报错,必须要进行强制类型转换

为什么任何类型的数据都可以存放是问题呢,我们的初衷不就是要创建一个可以存放任何类型数据的数组吗?

其实,更多情况下,我们只是希望能够接收所有数据,但是只能够持有一种数据类型而不是同时拥有这么多类型。所以,泛型的目的是:指定当前容器,要持有什么类型的对象,让编译器去检查。此处就要把类型当作参数,需要什么类型,就传入什么参数。

3.1泛型语法

class 泛型类名称<类型形参列表>{

        这里可以使用类型参数

}

修改为泛型则为

如果此时在main方法中,仍然有代码

myArray2.setArray(1,10);//在下标为1的位置插入整型10

String str2 = myArray2.getArray(1);

System.out.println(str2);

会出现如下报错

这就是引入泛型的好处,利用<T>,使得,这个数组成为一个可以接收任何类型的数组,然后我们再在使用这个类的时候,传入类参数,指定具体的类型String,之后编译器便会替我们自动检查类型是否正确

相关知识:

类名后的<T>代表占位符,表示当前类是一个泛型类

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

E 表示 Element    

K 表示 Key

V 表示 Value

N 表示 Number

T 表示 Type

4.泛型类的使用

4.1语法

泛型类<类型实参> 变量名; //定义一个泛型类引用

new 泛型类<泛型实参>(构造方法实参); //实例化一个泛型类对象

示例:

MyArray<String> list = new MyArray<String>(); //等号后面的<>中的String可省略,注意是<>中的可以省略,可不要全部省略为 new MyArray();

注意:泛型只能接收类,所有的基本数据类型必须使用包装类(这也是前面讲解包装类的原因)

4.2类型推导

编译器是可以根据上下文推导出类型实参的

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

小结:

        1.泛型是将类型参数化,进行传递

        2.使用<T>表示当前类是一个泛型类

        3.泛型优点:数据类型参数化,编译器自动进行类型检查和转换

5泛型是如何编译的???

5.1擦除机制

编译中,把所有的<T>替换为Object这种机制,就是擦除机制,这个机制较难理解,此处了解有这个机制即可。

6.泛型的上界

在定义泛型类的时候,有时需要对传入的类型变量做一定的约束

6.1语法

class 泛型类名称<类型形参 extends 类型边界>   //此处的extends不是继承的意思了,可以理解为扩展

实例

public class MyArray<T extends Number> {

}

MyArray泛型只能接受Number本身及其Number的子类作为E的类型实参,换句话说就是,T只能是Number或者是Number的子类

MyArray<Integer> list1; //right, Integer是Number的子类

MyArray<String> list2; //err,String不是Number的子类

如果没有指定边界,则默认为extends Object

6.2使用

写一个类,在类里面实现findMax方法

大家肯定都能写出这样的代码,大体上是正确的,但需要注意的是arr[i] > max这行代码发生了报错,为什么呢?

Object类的元素是无法进行比较的,前面了解了擦除机制,将所有的T都擦除为了Object,在Object中是没有比较的方法的,所以无法比较。

此时就可以使用泛型的上界了

T extends Compara<T> 表示 T 一定是实现Comparable接口的

实现Comparable接口,使得其Object元素可以进行比较,此时更能理解到extends可以翻译为拓展的意思了吧!!!

此时即可正常输出最大值98

如果任意有其他类Z,并未实现Comparable接口,则会直接报错,编译器会自动检查,并且及时报错

此时实现Comparable接口之后,则不会报错,可以正常编译

补充:静态的泛型方法

注意静态的泛型方法,需要在static后用<>声明泛型类型的参数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值