一.数据结构
- 什么是数据结构? 数据结构是计算机存储,组织数据的方式
数据结构分为三种:线性结构,树形结构,图形结构,每种结构包括的类型如下:
二.线性表
- 线性表是具有n个相同类型元素的有限序列(n>=0)
- a1是首节点(首元素),an是尾节点(尾元素)
- a1是a2的前驱,a2是a1的后继
- 常见的线性表有数组,链表,栈,队列,哈希表(散列表)
三.数组(Array)
- 数组是一种顺序存储的线性表,所有元素的内存地址也是连续的
- 数组缺点:无法动态修改数组容量
动态类设计需求 设计原则:
接口设计如下:
-
size 数组存放成员变量的个数
-
clear 数组成员变量为泛型时,如果是基本数据类型时,只设置size=0,如果为对象数据类型时,不仅要设置size=0,还要遍历循环设置数组每一个成员变量的值为null
-
add 默认将数据加到数组的最后一位。直接将element添加到index=size,然后size++
-
打印 重写对应的print方法,自定义打印内容,比如视频中讲到在java中打印数组,默认打印的是类名,默认调用数组的toString方法,如果想改变打印内容,重写toString方法,用StringBuilder自定义内容。了解思路即可
-
remove 数组成员变量为泛型时,如果是基本数据类型,删除index=n的数据时,将index=n后面的数据依次往前移动,移动范围为index=n-size-1;如果是对象数据类型,删除index=n的数据时,将index=n后面的数据依次往前移动,移动范围为index=n-size-1,然后size--,最后删除数组最后一个数组,这里size--的作用是通过size表示最后一个数组的下标值
-
add_index 首先在旧数组index=n的位置插入一个数据,先在堆空间中重新开辟一个内容,新size=旧size+1,然后遍历旧数组,遍历范围为(旧size-1)到index=n,将旧数组中index =(旧size-1)的数据移动到新数组size的位置,最后在新数组index=n的位置插入新数据
-
indexOf 如果传入数据为null,只需要将原数组遍历循环找到下标为null的index即可;如果传入数据不为null,将传入数据跟原数组中的数据进行遍历循环,通过equals方法比较是否相等,返回对应的index即可
-
接口测试 Assert测试接口,或者直接打印数据
-
动态扩容 动态扩容的思路是重新申请一块连续地址的内存空间,扩容的空间大小是原数组的n倍,然后将原数组中的数据拷贝到新数组中,销毁原数组的内存空间.
-
泛型 泛型的目的让动态数组更加通用,Array,此处的E就为泛型,java中的泛型只能放对象数据类型,基本数据类型不可用,例如Int不能用,要用Int的包装类Integer,int=integer。声明数组中要写明泛型类型,
-
对象类型 数组中存放对象类型数据时,实际存储分布为: 数组中存放的是对象的内存地址,内存地址又指向堆空间中分配的对象
- equals java(视频中以java语言为基础讲的内容)中通过equals方法,判断比较对象是否相等,如果没有重写equals方法,默认比较对象的内存地址,如果重写equals方法,则可以指定比较的类型
- null值处理
是否可以存储null的数据?答案是可以的,传入一个null ,默认index=n的位置上的数据为null,但是取出index=n的数据可能会报错,因为对象为null
- 动态数组有一个明显的缺点:可能会造成内存空间的大量浪费
四.链表(Linked List)
- 链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的
- 相比动态数组浪费空间,链表是用到时才会分配存储空间
- 数据存储表示如下:
-
动态设计链表 原则:在编写链表过程中,要注意边界测试,index=0,size,size-1等情况.
-
size
-
clear 首先size=0,然后设置first指针=null,不需要设置Node的next指向null
-
add 在index=n的位置插入一个Node元素,让index=n-1的Node中的next指针指向新插入的Node元素,新插入的Node元素中的next指针指向原index=n的Node元素,最后进行size++
-
remove 如果index=0,找到first,first.next.next指向index=index+1的位置;如果index!+0 删除index=n的元素,首先先获取index-1的元素,然后将获取的元素next指针指向index=n+1的元素
-
获取index对应的节点 获取index的节点,需要从first指针指向的第一个结点算起,index等于几就.next几次,遍历循环,i=0;i<index node = node.next ,返回最后的node即可。
-
indexOf 如果元素为null,遍历链表获取Node,然后对比Node里面存储的数据是否为null;如果不为null,遍历链表获取Node,然后对比Node里面存储的数据;
-
练习-删除链表中的节点 删除本节点的数值,改变本节点next指针指向,将要删除的node.next的下一个节点的值赋值给node,将node.next.next指针指向赋值给node.next指针
-
练习-反转一个单向链表 一般考虑反转单向链表的实现效果如下:
递归方式:
创建的这个算法
非递归方式: 首先声明一个newHead指针指向null,然后将head.next指针指向newHead指针指向的位置,然后将newHead指针指向head对应的节点,再将head指向head.next下一个节点,这里有一个问题,将head.next指向newHead的时候,下一个节点就没有指针牵引,所有我们在最初的时候创建一个tmp指针,指向head.next,所以具体代码看下图:
- 练习-判断一个链表是否有环
利用快慢指针来判断,如果有环,fast跟slow 会相遇,类似操场跑圈,否则没环。
视频中java代码如下:
注意点:while中的两个条件缺一不可,是为了避免fast或者fast.next为null后出现空指针异常的错误。
- 虚拟头节点 有时候为了让代码更加精简,统一所有节点的处理逻辑,可以在最前面增加一个虚拟的头节点(不存储数据) 结构图如下:
note:添加虚拟头节点之后,first指的是虚拟头节点,first.next表示index=0 的节点
- 数组复杂度分析 复杂度分析一般分为三个方向:最好情况复杂度,最坏情况复杂度,平均情况复杂度。 数组跟链表的复杂度分析主要分析增(add)删(remove)改(set)查(get)等方法。
先看数组:
get方法代码:
get方法时间复杂度为O(1)。
set方法代码:
set方法时间复杂度为O(1)。
add方法代码:
add方法时间复杂度分为三种:
最好的情况:在最后一位add元素,不需要移动元素,所有时间复杂度为O(1);
最坏的情况:在第一位add元素,需要移动整个数组元素,所有时间复杂度为O(n),此处的n代表的是数据规模,不是数据值,数组size是多少就移动多少次;
平均的情况:在中间任何位置add元素,需要移动的次数为:(1+2+3...+n)/n=1/2*n ,所有时间复杂度为O(n);
remove方法代码:
remove方法时间复杂度分为三种:
最好的情况:在最后一位remove元素,不需要移动元素,所有时间复杂度为O(1);
最坏的情况:在第一位remove元素,需要移动整个数组元素,所有时间复杂度为O(n),此处的n代表的是数据规模,不是数据值,数组size是多少就移动多少次;
平均的情况:在中间任何位置remove元素,需要移动的次数为:(1+2+3...+n)/n=1/2*n ,所有时间复杂度为O(n);
数组复杂度总结:如果是set/get元素,时间复杂度为O(1);如果是add/remove元素,时间复杂度平均情况为O(n)
-
链表复杂度分析
-
数组,链表复杂度比较结果如下
- 均摊复杂度 连续出现多次复杂度比较低的情况后,出现个别复杂度比较高的情况,使用均摊复杂度,离诶是最好情况下的均摊复杂度
-
数组的缩容 如果内存比较紧张,动态数据有比较多的剩余空间,可以考虑进行所容操作;
-
复杂度震荡
五.双向链表
可以提高链表的综合性
- clear
size=0;
first=null;
last=null;
- add
- remove
- 接口测试 Assert 测试
- 双向链表 VS 单向链表 同样的删除操作,复杂度相同的情况下,双向链表比单向链表的操作步骤少一半,所有双向链表效率高
- 双向链表 VS 动态数组
六.单向循环链表
-
add add的时候要考虑size=0 的时候
-
remove 要区分size=1 的情况
-
接口测试 Assert 测试
七.双向循环链表
- add
- remove
- 静态链表
- 数组的优化思路
五.栈(Stack)
- 1,栈的概念
- 栈是一种特殊的线性表,只能在一端进行
- 往栈中添加元素的操作,一般叫做push,入栈
- 从栈中移除元素的操作,一般叫做pop,出栈(只能移除栈顶元素,也叫做:弹出栈顶元素)
- 此处的栈跟栈空间是不同的概念,现在说的栈是一种数据结构,栈空间是一种内存
- 优先使用动态数组自定义栈
- 2.栈数据结构接口设计 根据push跟pop的原则,不希望设计那么稀奇古怪的接口
- 栈的应用-浏览器的前进和后退 利用两个栈存放数据
后退:将1中的栈顶元素弹出放进2中
前进:将2中的栈顶元素弹出放进1中
新输入数据:只要新输入元素,不管2中有多少数据,一律清空,将新输入元素放进1中,作为栈顶元素
- 练习-有效的括号 利用栈先进先出的特点来解答这个问题
代码实现
六.队列(Queue)
- 队列是一种特殊的线性表,只能在头尾两端进行操作
- 队尾(rear):只能从队尾添加元素,一般叫做enQueue,入队
- 队头(front):只能从队头移除元素,一般叫做deQueue,出队
- 先进先出的原则,First In First Out, FIFO
- 优先使用双向链表实现自定义队列,因为队列主要是往头尾操作元素,用双向链表实现的队列效率会高于用动态数组实现的队列
- 队列接口设计
- 练习——用栈实现队列 利用两个stack来做
七.双端队列(Deque)
-
双端队列是能在头尾两端添加,删除的队列
-
英文deque是 double ended queue 的简称
-
双端队列接口设计
八.循环队列(Circle Queue)
- 循环队列底层使用数组实现,也可以看作是数组的优化结果
- 循环队列默认是单端的
- 循环双端队列:可以进行两端插入,删除操作的循环队列
- 自定义循环队列代码
- 循环队列扩容 扩容原则是将原来队头(front)指向新内存的index=0 的位置,依次往队尾方向存储
九.循环双端队列
讲课见视频