一、数据结构与算法中一些重要东西
- 思维
- 空间换时间:使用存储空间对重复计算的值进行保存!避免重复计算,减低时间复杂度
- 升维
- 刷题技巧
- 自顶向下的编程方式:可以先把大的逻辑写完,在去编写细节,就不容易出错吧
- 不要去人肉递归
- 理解程序的本质:程序本身就是由三种结构组成的:顺序结构,条件结构,循环结构;因为计算机本身就是用来处理重复性问题的(当然,那些啥高端的东西就不清楚了哦)
- 理解算法题的本质:一般来说,计算机解决的问题是具有重复性的;因此对于特殊情况使用if解决,然后利用循环对重复问题进行解决;所以核心就是找到问题的重复性!
- 数据结构+算法;
一种数据结构:是由其组成结构+定义在其之上的操作进行组成的 - 学习数据结构和算法
对于某些困难的数据结构来说,学习他的由来、特性、适用场景以及他能解决的问题,很多时候就够了,毕竟以java举例,大部分的数据结构都已经封装在java.util集合里面了,我们只需要理解它的api调用使用即可。当然如果能自己实现当然更好
二、数据结构
- 按照数据与数据之间的关系进行分类
- 线性结构 (一对一)
- 树状结构(一对多)
- 网状结构(多对多)
- 按照存储结构的实现方式
- 线性
- 链式
- 按照存储空间的维度来分
- 一维
- 二维
- 特殊数据结构
这里按照数据与数据之间的关系来说:
线性结构
-
线性表:
线性实现就是我们常见的数组在java里面就是所谓的ArrayList;
而链式,最简单的链表则是LinkedList,但作为链表的话本身也具有很多种类:- 如果链表头和尾部进行连接则是循环链表
- 如果每个节点,与前后节点都相连接则是双向链表
- 如果同时包含上面两种特性则是双向循环链表当然在java集合的API中都有体现
-
队列,栈
首先要明白的是:队列,栈本质上是一种操作受限制的线性表。就是说他们和线性表可以共用一种结构,但定义在其之上的算法是有区别的
栈的限制:定义了只能从线性表的一端进行插入,删除操作,并且只能对栈顶元素进行访问;
栈的实现:
1. 链式:就是在链表的基础上,封装新的操作
2. 线性:就是在数组的基础上,封装新的操作
总结:栈:先进后出
队列的限制:定义了只能从线性表的一端进行插入,从另一端进行访问与删除
队列的实现:
1. 链式:就是在链表的基础上,封装新的操作
2. 线性:就是在数组的基础上,封装新的操作
但不过对于线性实现来说,如果一直添加,删除,然后就会产生空间的浪费。这时候就产生了一个新的方式:循环队列:线性队列的头部与尾部在逻辑上相连,可以想象成一个圆圈,就可以减少空间的浪费了;
总结:队列:先入先出;
优先级队列:普通队列的优先级,是谁先到谁优先级最大;当优先级的定义不在是谁先来,而是其他的东西的时候;
- 跳表
跳表并不像上面三种数据,那么的常见,也算是一种比较高级的数据结构了;
但不过其基础仍然是线性表中,由于需要对数据进行的增加删除较多,所以选用链表:但不过在他的基础上增加了约束和索引
约束:约束线性表中的元素始终是一个有序的状态;这就方便对序列进行二分查找;
由于二分查找的前提是元素可以凭借下标进行访问;但不过链表并不具备这一特性,因此就需要添加索引;
至于具体的细节,对于总览来说就不太过深究了
-
散列表:(Hash Table)
定义:散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。
具体表现:对于一个key值,由散列函数对其进行映射,转换为数组下标;然后我们就可以直接去数组中取得查询效率为O(1);
对于散列函数就有很多的考究了,这里就不细说了;
hash函数详解 -
线性实现与链式实现的对比
- 链表查询的时间复杂度较高,数组的插入和删除复杂度较高
- 数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。而链表内存中并不是连续存储,所以对CPU缓存不友好,没办法预读
- 数组的缺点是大小固定,一经声明就要占用整块连续的内存空间,如果声明数组过大,系统将可能没有足够的连续内存空间分配给他;就会导致内存不足!而链表则没有大小的限制
- 除此之外,如果代码对内存的使用非常苛刻,那么数组更适合;因为链表中的每个节点都需要消耗额外的存储空间去存储一份指向下一个节点的指针,内存消耗就会翻倍。而且对链表进行频繁的插入、删除操作,会导致频繁的内存申请和释放,容易导致内存碎片,对于java语言就可能导致频繁的GC
树状结构
在树状结构中最被人广为所知的就是二叉树
-
二叉树
定义:二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”和“右子树”;
基本概念:
1. 节点的高度:节点到叶子节点的最长路径
2. 深度:根节点到该节点所经历的边的个数
3. 层数:节点的深度+1
4. 树的高度:根节点到叶子节点的最长路径
树的特性
1. 一个二叉树第i层的最大节点为2e(i-1);
2. 深度为k的二叉树
3. 对任何非空二叉树T,若n0表示叶节点的个数,n2是度为2的非叶节点个数,那么两者满足关系n0 = n2 + 1
分类:
1. 满二叉树:叶子节点都在最底层;除叶子节点外,每个节点都有左右两个子节点
2. 完全二叉树:叶子节点在最底下两层,倒数第二层的叶子节点靠右排列,除了最后层,其他层的节点个数都要达到最大
树的实现:
1.链式存储:设置结构
public class node{int data ; node left,right}
对左右孩子进行引用
2. 顺序存储法:使用数组对数据进行存储,那如何描述他们的关系勒?
通过存储数组的下标进行表达;如节点的下标为i则其左孩子节点为i2则其右孩子节点为i2+1;其父节点为i/2
补充:使用数组存储完全二叉树是最节省内存的一种方式;
树的遍历:
1. 层次:使用队列对每一层进行遍历
2. 深度:根据根左右节点访问的先后顺序:分为先序,中序,后序遍历 -
二叉查找树:
特点:支持动态数据集合的快速插入、删除、查找操作;
二叉查找树,也和栈,队列一样是在二叉树的基础上,添加一些约束!
约束:
树的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值
支持的操作:(在约束下,会发现对于查找,删除,插入的操作,每下一层都会将数据量减少一半O(logN))- 查找
- 插入
- 删除
具体的代码实现这里就不多说了
二叉查找树具体代码
还有提一下:如果对于一个二叉查找树,如果删除的操作过于多;假设一种最坏的情况,根节点的左右子树极度不平衡,退化成链表,那么查找的时间复杂度就变成了O(n);
-
平衡二叉查找树:
从标题一看:平衡二叉查找树;就是在二叉查找树上增加了平衡二字;那么平衡二字代表了什么勒?实际上就是为了解决二叉查找树的问题;二叉查找树在频繁的动态更新过程中,可能会出现树的高度远大于log2n的情况(注意这个2是下标);极端情况下甚至可能会退化成链表,那么查询就比较慢了~因此就加上了平衡!- 什么是平衡二叉查找树
1). 二叉树中任意一个节点的左右子树的高度相差不能大于1(这就是平衡,因为)
2).并且符合二叉查找树的定义 - AVL树是严格符合上列定义的:是一种高度平衡的二叉查找树
- 平衡因子
定义:是它的左子树高度减去它的右子树高度,就是两棵树之差绝对值始终为1:(那么自然而然的,每个节点中就需要有多余的元素来进行记录) - 如何进行平衡——四大操作
1. 左旋 2.右旋 3.左右旋 4.右左旋
因为这是概述,所以只是大概描述:对于每一个元素在进行插入完毕后,就需要查看节点平衡因子的绝对值是否大于1;如果大于1就需要通过旋转操作将其保持在1!
-
不足:
节点需要存储额外信息,且调整次数频繁!因为其严格要求平衡,因此就相对应的在维持平衡上需要付出代价,带来的好处是,查找效率极高但不过实际上很多平衡二叉查找树其实并没有严格符合上面的定义;实际上二叉查找树很多。有些并不是严格要求平衡因此被称为近似平衡二叉树
- 平衡因子
- 红黑树
定义:
- 跟节点是黑色的
- 每个叶子节点都是黑色的空节点,也就是说,叶子节点不存储数据
- 任何相邻的节点都不能为红色
- 每个节点从该节点到达其可达的叶子节点的所有路径,都包含相同数目的黑色节点;
性质:
从跟到叶子最长路径不多于最短可能路径的两倍长; - 什么是平衡二叉查找树
在说说AVL和Red Black Trees的对比吧
1. AVL具有更高的查找特性,因为其具有更高的平衡性
2. 红黑树支持更快的插入和删除操作,因为AVL是严格要求平衡的
3. AVL要存储的额外信息比红黑树要更多一点点
经过以上对比相信大家对于各自的应用场景应该有数了吧。
这里还附带一个树状结构的常见用法:
字典树
字典树Trie详细链接
特殊数据结构
- 并查集
- 布隆过滤器