Java数据结构1

1. Java的集合框架

集合框架当中包含若干类,接口等组成的。集合类和接口的背后就是一个一个的数据结构。

例:Java中Stack的集合类的背后的数据结构就是栈
在这里插入图片描述
图片来源:https://www.runoob.com/java/java-collections.html

2. 集合框架的重要性

2.1 开发中使用

  • 使用成熟的集合框架,有助于我们便捷快速的写出高效且稳定的代码
  • 学习背后的数据结构,有助于我们理解集合类的优缺点与其使用场景

2.2 笔试和面试

必考

3. 背后的数据结构与算法

3.1 什么是数据结构

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

3.2 Java容器背后对应的数据结构

Java 容器(Collections)在其背后使用了多种数据结构,每种数据结构都有其独特的特性和用途。下面是一些常见的Java容器及其背后的数据结构:

3.2.1 List 接口及其实现类

  • ArrayList: 底层是一个动态数组(Object[]),支持随机访问,增删操作效率不高(因为需要移动元素)

  • LinkedList: 底层是双向链表,增删操作效率高(因为只需要调整指针),但是随机访问效率低

3.2.2 Set 接口及其实现类

  • HashSet: 底层是哈希表(基于 HashMap 实现),不保证元素顺序,插入、删除、查找操作时间复杂度为 O(1)

  • LinkedHashSet: 底层是链表和哈希表的结合,既保证了元素的插入顺序,又保留了哈希表的快速访问特性

  • TreeSet: 底层是红黑树(自平衡二叉搜索树),保证元素有序,增删查操作时间复杂度为 O(log n)

3.2.3 Map 接口及其实现类

  • HashMap: 底层是哈希表(数组+链表/红黑树),不保证键值对顺序,插入、删除、查找操作时间复杂度为 O(1)
  • LinkedHashMap: 底层是哈希表和双向链表的结合,保证了键值对的插入顺序。
    TreeMap: 底层是红黑树,保证键值对有序,增删查操作时间复杂度为 O(log n)
  • Hashtable: 底层也是哈希表,但它是线程安全的,不建议在高并发环境下使用,通常会用 ConcurrentHashMap 替代

3.2.4 Queue 接口及其实现类

  • PriorityQueue: 底层是一个基于数组的二叉堆,支持优先级排序,插入和删除操作时间复杂度为 O(log n)
  • ArrayDeque: 底层是一个循环数组,支持双端队列操作,插入和删除操作时间复杂度为 O(1)

3.2.5 Deque 接口及其实现类

  • ArrayDeque: 同上,底层是一个循环数组
  • LinkedList: 同上,底层是一个双向链表

3.3 相关java知识

  • 泛型 (Generic)
  • 自动装箱 (autobox 和自动拆箱 (autounbox)
  • Object 的 equals 方法
  • Comparable 和 Comparator 接口

3.4 什么是算法

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

3.5 如何学好数据结构以及算法

  • 码代码
  • 画图和思考
  • 博客
  • 刷题

4. 如何衡量一个算法的好坏

判断某一个算法好还是不好,为什么?该怎么衡量一个算法的好坏呢?

5. 算法效率

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

6. 时间复杂度

6.1 时间复杂度是什么

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

简单来说,时间复杂度(Time Complexity)是一个算法在执行过程中所需时间的度量,主要用于描述算法在输入规模变化时,其执行时间如何变化。它通常用大 O 记号(Big O notation)来表示,来估算算法在最坏情况下的执行时间。

6.2 大O表示法

大O表示法(Big O notation)是一种用于描述算法复杂度的数学符号,用于分析算法的性能或运行时间。大O表示法主要关注输入规模趋近于无穷大时的增长趋势,忽略低阶项和常数因子。

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

6.2.1 大O表示法的定义

对于一个函数 f ( n ) f(n) f(n),如果存在正数 c c c正整数 n 0 n_0 n0使得对于所有的 n ≥ n 0 n \geq n_0 nn0,都有 f ( n ) ≤ c ⋅ g ( n ) f(n) \leq c \cdot g(n) f(n)cg(n),那么 f ( n ) f(n) f(n)属于 O ( g ( n ) ) O(g(n)) O(g(n)),即: f ( n ) = O ( g ( n ) ) f(n)= O(g(n)) f(n)=O(g(n))

6.2.2 大O的渐进表示法

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

6.2.3 推导大O阶方法

  1. 用常数1取代运行时间中的所有加法常数
  2. 忽略常数项:在大O表示法中,常数项对复杂度的影响被忽略。例如,O(2n)和O(3n)都被简化为O(n)。
  3. 忽略低阶项:只关注最高阶项,因为它对复杂度的增长影响最大。例如,O(n^2 + n)被简化为O(n^2)

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

另外有些算法的时间复杂度存在最好、平均和最坏情况:

  • 最坏情况:任意输入规模的最大运行次数(上界)
  • 平均情况:任意输入规模的期望运行次数
  • 最好情况:任意输入规模的最小运行次数(下界)

6.2.4 常见的大O时间复杂度

  1. O(1) - 常数时间复杂度
    • 操作的执行时间不随输入规模的变化而变化
    • 例子:访问数组中的某个元素
  2. O(log n) - 对数时间复杂度
    • 操作的执行时间随输入规模的增加而对数增长
    • 例子:二分查找
  3. O(n) - 线性时间复杂度
    • 操作的执行时间与输入规模成正比
    • 例子:遍历一个包含 n 个元素的数组
  4. O(n log n) - 线性对数时间复杂度
    • 操作的执行时间随输入规模的增加而线性对数增长
    • 例子:快速排序、归并排序
  5. O(n^2) - 平方时间复杂度
    • 操作的执行时间与输入规模的平方成正比
    • 例子:冒泡排序、选择排序
  6. O(2^n) - 指数时间复杂度
    • 操作的执行时间随输入规模的增加而指数增长
    • 例子:解决背包问题的递归算法
  7. O(n!) - 阶乘时间复杂度
    • 操作的执行时间随输入规模的增加而阶乘增长
    • 例子:解决旅行商问题的暴力搜索算法

6.3 如何分析时间复杂度

分析算法的时间复杂度是评估其效率的关键步骤。以下是分析时间复杂度的详细步骤和方法

1. 确定基本操作

基本操作是算法中执行次数最多的操作,通常是一个简单的、可重复的操作,比如赋值、比较、算术运算等

2. 计数基本操作的执行次数

根据输入的规模 n n n,计算基本操作执行的次数。通常情况下,执行次数可以表示为一个关于 n n n的函数

3. 忽略低次项和常数项

在时间复杂度分析中,我们关注的是随着输入规模 n n n增大,运行时间的增长趋势。因此,我们忽略常数项和低次项,只关注最高次项。

4. 使用大O符号表示时间复杂度

例子分析

例子1:线性搜索
public int linearSearch(int[] arr, int x) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == x) {
            return i;
        }
    }
    return -1;
}

分析:

  1. 基本操作是比较 a r r [ i ] = = x arr[i] == x arr[i]==x
  2. 在最坏情况下,需要进行 n 次比较
  3. 忽略常数项,时间复杂度是 O(n)
例子2:冒泡排序
public void bubbleSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 交换 arr[j] 和 arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

分析:

  1. 基本操作是比较和交换 a r r [ j ] > a r r [ j + 1 ] arr[j] > arr[j+1] arr[j]>arr[j+1]
  2. 双重循环:外层循环 n n n 次,内层循环平均 n / 2 n/2 n/2
  3. 总的比较次数约为 n ( n − 1 ) / 2 n(n−1)/2 n(n1)/2
  4. 忽略常数项和低次项,时间复杂度是 O ( n 2 ) O(n^2 ) O(n2)
例子3:二分查找
public int binarySearch(int[] arr, int x) {
    int l = 0, r = arr.length - 1;
    while (l <= r) {
        int m = l + (r - l) / 2;
        if (arr[m] == x)
            return m;
        if (arr[m] < x)
            l = m + 1;
        else
            r = m - 1;
    }
    return -1;
}

分析:

  1. 基本操作是比较 a r r [ m ] = = x arr[m] == x arr[m]==x
  2. 每次比较后,搜索范围减半
  3. 在最坏情况下,比较次数为 l o g n logn logn
  4. 时间复杂度是 O ( l o g n ) O(logn ) O(logn)
例子4:斐波那契数列
public int fibonacci(int n) {
    if (n <= 1) {
        return n;
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}
  1. 基本操作是加法运算 f i b o n a c c i ( n − 1 ) + f i b o n a c c i ( n − 2 ) fibonacci(n - 1) + fibonacci(n - 2) fibonacci(n1)+fibonacci(n2)
  2. 在每次递归调用中,问题的规模减小了一
  3. 在最坏情况下,需要进行 2 n 2^n 2n 次递归调用
  4. 时间复杂度是 O ( 2 n ) O(2^n) O(2n)。这种指数级别的时间复杂度使得这个算法在 n n n较大时非常低效
总结

分析Java代码的时间复杂度与其他语言的过程相似,关键在于确定基本操作,计算这些操作随输入规模增长的执行次数,并使用大O符号表示时间复杂度。这种分析有助于我们理解和比较不同算法在处理大规模数据时的效率。

7. 空间复杂度

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

在分析算法的空间复杂度时,主要考虑以下几种情况:

7.1 常见的大O空间复杂度

1. 常量空间复杂度 O ( 1 ) O(1) O(1)

算法使用固定大小的内存空间,不随着输入规模的增加而改变。典型的例子包括迭代算法和一些基本的数据结构操作,如数组、变量等。

2. 线性空间复杂度 O ( n ) O(n) O(n)

算法的额外空间需求随着输入规模的增加而线性增加。典型的例子包括一些递归算法、使用额外数组或列表存储数据等。

3. 递归调用空间复杂度

递归算法的空间复杂度通常取决于递归调用的深度。每次递归调用都会将一部分内存压入堆栈,直到递归结束返回。因此,递归算法的空间复杂度通常与递归的最大深度成正比。

4. 复杂空间复杂度

某些算法可能会使用更复杂的数据结构,如树、图等,其空间复杂度取决于所使用的数据结构的大小和结构特点。

7.2 示例分析

例子1:线性搜索

public int linearSearch(int[] arr, int x) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == x) {
            return i;
        }
    }
    return -1;
}

空间复杂度为 O ( 1 ) O(1) O(1),因为只需要额外的常量级别的内存空间来存储少量的变量。

例子2:递归求阶乘

public int factorial(int n) {
    if (n == 0) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

空间复杂度取决于递归调用的深度,最大深度为 n n n,因此空间复杂度为 O ( n ) O(n) O(n)

例子3:归并排序

public void mergeSort(int[] arr, int l, int r) {
    if (l < r) {
        int m = (l + r) / 2;
        mergeSort(arr, l, m);
        mergeSort(arr, m + 1, r);
        merge(arr, l, m, r);
    }
}

在归并排序中,除了存储原始数组外,还需要额外的空间来存储临时的子数组。每次递归调用时,需要额外的 O ( n ) O(n) O(n) 空间来存储子数组。因此,空间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

8. 包装类

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

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

除了 Integer 和 Character, 其余基本类型的包装类都是首字母大写。

8.2 装箱和拆箱

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

8.3 自动装箱和自动拆箱

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

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

在汇编层面,上述代码将会被转换成类似以下的指令:

invokestatic java/lang/Integer.valueOf(I)Ljava/lang/Integer;// 自动装箱
invokevirtual java/lang/Integer.intValue()I// 自动拆箱

总的来说,自动装箱和自动拆箱是Java语言的一种语法糖,使得基本数据类型和包装类型之间的转换更加方便,但在汇编层面,实际上是通过调用相应的方法来实现的。

【面试题】

下列代码输出什么,为什么?

public static void main(String[] args) {
  Integer a = 127;
  Integer b = 127;
  Integer c = 128;
  Integer d = 128;
  System.out.println(a == b);
  System.out.println(c == d);
}

这段代码输出为:

true
false

这是因为Java中存在一个整数缓存范围,默认情况下是 -128 到 127。当使用自动装箱创建一个值在这个范围内的 Integer 对象时,会尝试从缓存中获取已存在的对象,而不是每次都创建新的对象。

所以,对于 Integer a = 127; 和 Integer b = 127; 这两行,a 和 b 都被指向了同一个缓存中的对象,因此 a == b 输出为 true。

但是,对于 Integer c = 128; 和 Integer d = 128; 这两行,由于 128 超出了缓存范围,所以每次都会创建一个新的对象,而这两个对象在内存中的地址不同,因此 c == d 输出为 false。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值