java 数据结构笔记

目录

 

前言

复杂度

时间复杂度

空间复杂度

数组

链表

数组和链表对比

队列

散列表

二叉树

平衡二叉树/红黑树

跳表


前言

数据结构就是指一组数据的存储结构,算法就说操作数据的一组方法。

数据结构和算法相辅相成。

数据结构为算法服务,算法则需要作用在指定数据结构之上。


复杂度

数据结构与算法解决的问题是如何更快,更省的存储和处理数据,那么我们就需要一个考量资源消耗和效率的方法,就说复杂度分析。

从低阶到高阶的复杂度有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)。

时间复杂度

概念介绍

表示算法的执行时间与数据规模之间的关系称为时间复杂度

所有代码的执行时间T(n)和每行代码的执行次数n成正比。所以可以总结为:T(n) = O(f(n))

  • T(n)代表代码执行的时间,也就是时间复杂度。
  • f(n)表示每行代码执行的次数总和。
  • O表示代码的执行时间T(n)和f(n)表达式成正比。也就是大O时间复杂度表示法。

分析逻辑

1.总复杂度等于量级最大的代码的复杂度。

2.只要代码的执行时间不随n的增长而增长,时间复杂度则为O(1)。

举例

1 int cal(int n) {
2    int sum_1 = 0;
3    int p = 1;
4    for (; p < 100; ++p) {
5      sum_1 = sum_1 + p;
6    }
7 
8    int sum_2 = 0;
9    int q = 1;
10    for (; q < n; ++q) {
11     sum_2 = sum_2 + q;
12    }
13  
14    int sum_3 = 0;
15    int i = 1;
16    int j = 1;
17    for (; i <= n; ++i) {
18      j = 1; 
19      for (; j <= n; ++j) {
20        sum_3 = sum_3 +  i * j;
21      }
22    }
23     i=1; 
24     while (i <= n) 
25        { i = i * 2; }
26  
27    return sum_1 + sum_2 + sum_3;
28  }

 

  • 2,3,4,5,8,9行   的时间复杂度为O(1)
  • 10,11行   的时间复杂度为O(n)
  • 14,15,16行   的时间复杂度为O(1)
  • 17,18行   时间复杂度为O(n)
  • 19,20行   的时间复杂度为O(n^2)
  • 24,25行  的时间复杂度为O(logn)

所以取量级最大的代码的复杂度则上述代码的时间复杂度为O(n^2)。

空间复杂度

概念介绍

表示算法的执行时间与存储空间之间的关系称为空间复杂度

常见的空间复杂度有 O(1)、O(n)、O(n2 )

分析逻辑

1.同时间复杂度类似

2.判断代码所需存储空间。

举例


void print(int n) {
  int i = 0;
  int[] a = new int[n];
  for (i; i <n; ++i) {
    a[i] = i * i;
  }

  for (i = n-1; i >= 0; --i) {
    print out a[i]
  }
}

 空间复杂度为: O(n)


 数组

概念

数组(Array)是一种线性表数据结构,它是用一组连续的内存空间来存储数据。

线性表

线性表就是数据在二维空间表示的结构,数据排成一条线,每个线性表上的数据最多只有前和后两个方向。其实,除了数组,链表,队列,栈等都是线性表结构二叉树,堆,图等这样子数据之间并不是简单的前后关系的为非线性结构

连续的内存空间

因为连续的内存空间的特性

1.在数组中进行插入,删除操作,为了保证连续性,就需要做大量的数据搬移工作。

2.随机访问。计算机会给该连续空间分配地址,可以通过连续空间的首地址 + 元素所需字节数(比如int,4个字节)* 元素在数组中的位置数 来计算的出该元素所在的空间地址。所以可以达到随机访问的特性

3.对内存要求高,如果没有连续的要申请的内存大小,可能会申请失败

复杂度分析

数组随机访问的时间复杂度为O(1)。

数组的插入和删除操作最坏则是在数组开头操作,那样则需要将所有数据往后移或者往前移,时间复杂度则为O(n)。

 扩展

1.基于数组实现的ArrayList等容器与数组比较

  1. 容器最大的优点就是将很多数组操作的细节封装起来
  2. 容器支持动态扩容。比如arraylist每次存储空间不够的时候,就会将空间自动扩容为1.5倍大小。而数组需要预先指定大小
  3. 容器比如ArrayList无法存储基本类型,需要包装类。所以性能会稍微比数组低一点。如果特别关注性能,或者希望使用基本数据类型,就选用数组。

2.为什么数组的下标要从0开始

在进行计算某个元素的内存地址的时候公式会从:

a[k]_address = base_address(内存首地址) + k * type_size(元素类型所占字节数)

变为

a[k]_address = base_address(内存首地址) + (k-1) * type_size(元素类型所占字节数)

这样对于CPU来说,多了一次减法指令

其次,就是效仿C语言,沿用了从0计数的习惯。


链表

概念

链表是一种线性表结构。通过指针将一组零散的内存块串联在一起

分类

常见的链表结构有: 单链表,双向链表,循环链表

单链表

头部节点指向的是链表的基地址,尾部节点指向空地址NULL。

复杂度分析

因为链表的存储空间本身不是连续的,而链表对随机访问的性能并不好需要遍历,否则得不到对应的元素地址,则链表随机查询对应的时间复杂度为O(N).如果给定前驱节点,则插入,删除时间复杂度为O(1)。所以单纯的链表插入删除时间复杂度为O(1),但是如果没有给定,则需要遍历,那么链表的插入,删除时间复杂度为O(n).

循环链表

和单链表唯一的区别在于尾节点。循环链表的尾节点指向头节点。

优点则是从链尾到链头比较方便,当要处理的数据据有环形结构特点时即可使用循环链表。

双向链表

举例:LinkedHashMap

支持两个方向,每个节点不仅有后继指针next指向后面的节点,还有指向前面的prev节点。所以双向链表要比单向链表占用更多的内存。

时间复杂度

双向链表的删除操作,可以根据要删除的节点得到前驱节点,则不需要遍历,删除时间复杂度为O(1).

其他情况下,和单向链表一样。所以双向链表的优势在于可以通过O(1)复杂度找到前驱节点

 扩展

我们可以在链表的基础上,运用链表实现LRU淘汰策略。越靠近链表尾部的节点越早之前访问的,当要查询链表某个数据时,可以从头部开始遍历,因为头部都是最新的数据,如果此数据已经被缓存在链表中,将原位置删除,然后再插入到链表头部。如果此数据在链表中不存在,(链表没满,将该数据直接插入到头部。链表已满,将尾节点删除,插入到头部)


数组和链表对比

1.数组因为使用的连续内存空间,可以借助CPU的缓存机制,预读数组中的数据,访问效率更高。链表在内存中不是连续的,所以对CPU缓存不太友好,没办法有效预读。(CPU每次从内存读取数据时,会先将数据加载到CPU缓存中,而每次CPU不是只读取特定访问的地址,每次是读取一个数据块,并保持到CPU缓存中)

2.数组大小固定。一旦申请就需要一块连续的内存空间来占用。链表天然支持动态扩容。(ArrayList占满也会重新申请1.5倍的大小来将原数组数据拷贝过去)

3.链表占用内存更大些。因为每个节点都要存储下个节点的指针。


栈:后进者先出,先进者后出。也是一种线性表结构。只允许在一端插入和删除数据。入栈(在栈顶插入)和出栈(在栈顶删除

实际上, 栈既可以通过数组实现(顺序栈),也可以通过链表实现(链式栈)。

复杂度分析

不管是入栈还是出栈,时间复杂度都是O(1)。

不管是在入栈还是出栈的过程,只需要一两个变量的存储空间,空间复杂度也都是O(1)。

扩展

操作系统给每个线程都会分配一块独立的内存空间,这块内存被组织成栈结构,比如函数调用等操作符合后进先出的特性。本质相同。


队列

队列: 先进者先出。

队列也只支持入队(放一个数据到队列尾部)和出队(从队列头部取一个元素)。

所以,队列和栈一样,也是一种操作受限的线性表结构。

队列的应用再平常工作中有很多,比如循环队列,阻塞队列,并发队列。环形缓存等等。

实际上,队列也是既可以通过数组实现(顺序队列),也可以通过链表实现(链式队列)。

时间复杂度分析

基于数组实现在数组长度达到的时候需要扩容,所以,我们选择在每次入队的时候判断空间是否够用,不够就将数组迁移到新扩容的数组上,这样,出队的时间复杂度任然为O(1).但是入队在队满的时候的时间复杂度则变为了O(n),其他情况任是O(1).

基于链表实现,出队入队时间复杂度为O(1)

区别

基于链表可以实现一个支持无限排队的队列,但是会导致过多的请求排队等待,针对响应时间敏感的系统则无限排队是不合适的。

基于数组的队列大小有限,可以合理设置大小,队列满了可以进行阻塞


 

散列表

基于数组支持随机访问的特性,比如我们常常使用的HashMap则是使用散列表实现的。

但是存在散列冲突的问题。解决散列冲突的方法可以通过开放寻址法(发现冲突后,从当前位置往后一次查找,如果有空闲位置则插入。),或者链表法。hashMap则使用了链表法。

散列表在不扩容的情况下查询的时间复杂度为O(1).扩容则是O(N)。

LinkedHashMap是通过散列表+双向链表+LRU淘汰算法实现的。

散列表:插入删除查找都是O(1), 是最常用的,但其缺点是不能顺序遍历以及扩容缩容的性能损耗。适用于那些不需要顺序遍历,数据更新不那么频繁的。


二叉树

顾名思义,每个节点最多有两个叉,也就是左子节点和右子节点。

二叉树中有两种特殊的二叉树,满二叉树和完全二叉树。

二叉树存储可以有两种方式,1.基于指针的链式存储。2.基于数组的顺序存储

每个节点除了存储数据还有指向左右子节点的指针。基于链式存储大部分都是通过这种结构实现的。

基于数组存储则根据下标计算左右位置,比如下标i的左节点就会是2*i,下标为i的右节点就为2*i+1.通过下标计算就可以把整个树串起来。

如果二叉树是完全二叉树,也就是除了最后一层其他层的节点个数都达到了最大,数组存储更省内存,因为数组根据下标计算,是连续空间存储,那么除了完全二叉树,其他情况空的节点也需要消耗多余的存储空间。所以,完全二叉树使用数组顺序存储更省空间。其他则不。这也是为什么完全二叉树要求最后一层的子节点都靠左的原因。

二叉树遍历的时间复杂度为O(n)。

数组顺序存储的方式比较适合完全二叉树,其他类型的二叉树用数组存储会比较浪费存储空间

二叉树的弊端:

查找性能偏低。

平衡二叉树/红黑树

因为二叉树的操作和树的高度成正比,为了降低二叉树的高度,查找操作的时间复杂度都是 O(logn)。

红黑树:插入删除查找都是O(logn), 中序遍历即是顺序遍历,稳定。缺点是难以实现,去查找不方便。其实跳表更佳,但红黑树已经用于很多地方了。

 

跳表

跳表是在链表之上加上多层索引构成的

跳表:插入删除查找都是O(logn), 并且能顺序遍历。缺点是空间复杂度O(n)。适用于不那么在意内存空间的,其顺序遍历和区间查找非常方便。

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

刘狗

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值