- 数据结构:研究数据在内存中存储的结构
- 算法:实现增删改查的方法
- 解决问题的实际方法
- 算法的好坏评价标准:时间复杂度和空间复杂度
- 时间复杂度:算法所用时间的一个映射
- 时间复杂度得出的是一个数学问题
- 数据总量 x (x 足够大),从而消耗的执行次数 y 之间存在的关系
LeetCode 算法题目:
- 达到一百道题目,一天三道题
- 达到三百道题目,一天五道题
- 达到五百道题目,一天十道题
数组代码讲解:
- 当创建对象时,对象里面的所有东西都会被创建
- 当有数组时,也会创建,基本类型数组默认值为 0
- 当方法操作的全局变量时只能操作对象里的变量
- 当引用类型不再被指引,Java 会触发回收机制
- 大括号:在遇到 } 时,里面新声明的变量要出栈
- 数组:
- 数组删除数据不是弹栈形式,而是改变有效边界,此数据还在该位置,只是无效。等下一个数据直接覆盖它
- 基本数据类型的一维数组:物理地址必须连续
- 其他复杂型数组逻辑上连续
- 无序数组:
- 删除:直接把最后的数据和删除的数值交换,再改变边界值
- 内存的重要性和代码速度严格挂钩
数组:
- 无序数组:
- 查询时间复杂度:O(n)
- 有序数组:
- 二分查找时间复杂度:O(log n)
- 当无序数组变得有序数组之后,需要消耗 O(N log n) 的时间,有些得不偿失
- 时间复杂度大小关系:
- O(1) < O(long n) < O(n) < O(n long n) < O(n^2)
- 哈希表(H):
- 通过 n % arr.length 公式为下标存储数据
- 二叉树:
- 特点:每一个叶子节点的路径都是不重复的
- 满二叉树、完全二叉树、···
- 时间复杂度不稳定:介于 O(log n) - O(n)
- 平衡二叉树:
- 在二叉排序树的基础之上而来
- 要求左右子树的高度(层数) 差绝对值不能超过 1,一旦超过就会触发平衡策略
- 平衡策略:LL、LR、RL、RR 型旋转
- 不平衡时:
- 时间复杂度:接近 O(log n)
- 平衡调整太浪费系统资源
- 步骤:
- 不平衡的点朝着造成不平衡的节点走两步
- 选择造成不平衡最近的节点进行处理:
- RL旋转:
- LR 旋转:后两个节点整体旋转
- 红黑树:
- 最优二叉树 ---> 内存最优树
- 时间复杂度:接近 O(log n),且比较稳定
- 特点:
- 红黑树的跟节点一定是黑色的
- 红黑树的叶子节点是黑色的(系统自带的叶子节点)
- 从根节点出发,到任意一个叶子节点,所走过的路径上黑色的数量是相同的
- 红色节点的下一个节点一定是黑色节点
- 结论:
- 最长路径不会超过最短路径的 2 倍
链表:
- 链表是由节点组成
- 创建链表:
- 单项链表:有数值域和 next 域
- value:存储此节点的值
- next:存储下一节点的地址
- 构造器:
- 创建对象的时候,给对象赋初始值
- 名字与类名相同,没有返回值
- 引用数据类型在内存中存储地址时,必须和自己本身类型相同
- 在内存中存储方式相同
排序算法:
- 冒泡排序:
- 前后两个数值之间进行对比排序
public void sort(int[] arr){ for (int i = 0; i < arr.length; i++) { for (int j = 0; j < arr.length - 1 - i; j++) { if(arr[j] > arr[j+1]){ int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } } }
- 前后两个数值之间进行对比排序
- 快速排序:
public static void sort(int[] arr,int left,int right){ if(left >= right){ return; } int i = left; //定义左游标 int j = right; //定义右游标 int base = arr[left]; //定义基准数 while(i != j){ while (arr[j] >= base && i<j){ //找到比基准数小的值 j--; } while (arr[i] <= base && i<j){ //找到比基准数大的值 i++; } swap(arr, i,j); //交换两值 } //交换基准数 arr[left] = arr[i]; arr[i] = base; sort(arr,left,i-1); //排序左数组 sort(arr,i+1,right); //排序右数组 } public static void swap(int[] arr,int i,int j){ int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
- 选择排序(简单选择排序):
- 找到待排序数组中最小的值,将其放入待排序数组的首位进行交换
- 最小值:比较查找
- 找到待排序数组中最小的值,将其放入待排序数组的首位进行交换
- 插入排序:
- 把当前排序数组中的值插入到已经排好的数组中
- 假设数组中第一个元素是已经排好序的
- 在待排序数组中挨个插入,如果比排好序的数值大,就将数值进行交换,再插入数值
- 缺点:
- 插入排序过程中,如果后边的数值比较小,前边的数值比较大
- 就可能导致小数据向前移动次数比较多,进而把整个算法的效率降低
- 把当前排序数组中的值插入到已经排好的数组中
- 希尔排序:
- 将小数据尽量向前移动
- 步骤:
- 分组根据数组长度而定,一般是:2,4,6,8,···
- 两两分组,间隔是数组长度的一半
- 四四分组,在组内进行排序
- ·······
- 注意递归停止条件,注意越界
- 步骤1:
- 假设将待排序数组中的第一个数作为基准数
- 定义 i,j 游标,分别指向待排序数组的第一位和最后一位
- 让 j 游标先移动,去找比当前基准数小的数据,找到后停止
- 让 i 游标移动,找比当前基准数大的数据,找到后停止
- 判断两个游标是否相遇:
- 未相遇:i 和 j 指向的值进行交换,继续执行3,4,5步
- 相遇:相遇位置的数和基准数进行交换
- 此时基本数的左边都比基准数小,右边都比基准数大
- 步骤2:
- 以基本数为界,将数组拆分左右两部分,重新执行步骤1
- 当拆分成每个数组只有一个值时,就变得有序了
- 时间复杂度:n log n
- 归并排序:
- 对数组进行拆分,当拆分的数组只有一个元素的时候停止
- 开始向上排序合并
- 基数排序(桶的思想):
- 有九个桶
- 先按个位排序,将个位相同的数据放入相同的桶
- 然后按桶的从左向右顺序,从一个桶中的数据从小到大挨个取出组成数组
- 再按十位排序,将十位相同的数据放入相同的桶,再和同上步骤取出
public static void radixSort(int[] arr) { //找最大数 int max = arr[0]; for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } int count = (max + "").length(); //排序次数 int[][] sorkets = new int[10][arr.length]; //十个桶 int[] sorketcount = new int[10]; //每个桶有效个数 for (int k = 0, n = 1; k < count; k++, n *= 10) { //控制排序次数 //将元素放入桶 for (int j = 0; j < arr.length; j++) { int num = arr[j] / n % 10; sorkets[num][sorketcount[num]++] = arr[j]; } //取出元素 int index = 0; for (int i = 0; i < sorketcount.length; i++) { //判断当前桶中是否有元素 if (sorketcount[i] != 0) { for (int j = 0; j < sorketcount[i]; j++) { arr[index++] = sorkets[i][j]; } sorketcount[i] = 0; //取空元素后将有效个数置为0 } } }
递归算法:
- 本质:
- 方法调用其本身(有爆栈情况)
- 必须有判断方法的出口
- 作用:
- 做循环
- 递归的思想:
- 反向思考
- 递归的实现:
- 确定递归函数的参数和返回值以及返回值类型
- 变量有哪些参数就有哪些
- 找到递归的终止条件
- 递归的逻辑实现
- 确定递归函数的参数和返回值以及返回值类型
哈夫曼编码以及二叉树的遍历:
- 哈夫曼编码:
- 来自于哈夫曼树(本质是二叉树)
- 对传输的数据进行压缩
- 哈夫曼树:
- 特点:
- 哈夫曼二叉树的特点:
- 整棵树的带权路径和是最小的
- 带权路径:权值 * 从根节点开始到节点走过的节点树
- 要求所有子节点都必须有权值:
- 权值越大的值离根节点越近
- 权值的意义:
- 当权值越大,分配的路径越短
- 哈夫曼二叉树的特点:
- 特点:
- 哈夫曼树的构建:
- 从权值小的开始向上构建
- 哈夫曼编码的构建:
- 假设左边走是1,右边走是 0
- 到 y 的路径就是:1001
- 二叉树的遍历:
- 深度优先遍历:
- 先序遍历:根节点先左子树,再右子树
- 中序遍历:先左,后中,再右遍历
- 后序遍历:先左,后右,再中遍历
- 广度优先遍历:
- 借助队列进行:先进先出
- 深度优先遍历:
栈和队列:
- 栈:
- 特点:先进后出
- 实现:数组或链表实现
- 出栈没有真正意义的删除,只是游标向前移动
- 队列:
- 本质:先进先出
泛型:
- 泛型:泛指一切类型
- 使用方法:
- 在类名后面加 <E> 符号
- 在创建对象时要声明创建的类型
- 创建对象的类型应该是包装类型
B树:
- 作用:主要用于磁盘当中做数据查询
- 特点:
- 高度低,节约寻址次数
- 每个节点最多有 M-1 个 key 值,并且可以升序排列
- key值:帮助节点排序
- 每个节点最多能有 M 个子节点
- 根节点至少有两个子节点
- 和红黑树对比:
- B树:节省寻址次数,增加对比次数
- 红黑树:节省对比次数,增加寻址次数
- 优先级:
- 每个节点都包含 key 和 value,因此我们根据 key 查找 value 时,只需要找到 key 点在的位置,就能找到 value 值
- 但 B+ 树只有叶子节点存储数据,索引每一个次查找,都必须一次一次的查找,一直找到树的最深处,也就是叶子节点的深度,才能找到 value 值
B+树:
- 特点:
- 非叶子节点只能存储 key 值,不存储 value 值
- 树的所有叶子节点构成一个有序链表,可以按照 key 排序的次序依次遍历全部数据
- 优点:
- B+ 树非叶子节点上不包含真正的数据,只当做索引使用,因此在内存相同的情况下,能存放更多的 key 值
- B+ 树的叶子节点都是相连接的,因此对整棵树的遍历只需要一次线性遍历叶子节点即可
- 由于数组顺序排列并且相连,所以以便于区间查找和搜索。
- 而 B树则只需要进行每一层的递归遍历
- B+ 树是数据库的底层数据结构
- 来提高数据库的查询效率
- B+ 树在数据库中的应用
- 可以基于某张表的某个字段建立索引,提高查询效率
- 未建立索引查询:
- 查询数据时需要从第一条数据开始,一直查询找到该值才停止
- 建立索引:只要找到比查找值小的节点的 key 值,依次向后遍历链表查找即可,效率非常高
有序二叉树的构建和遍历:
- 构建方式:
- 递归方式
- 非递归方式
- 步骤:
- 首先判断当前节点要和插入的节点的大小
- 如果小于当前节点,判断左子树是否有数据
- 没有数据时直接插入
- 如果有数据,向左走,重复上述 1,2 步骤
- 如果大于当前节点,判断左子树是否有数据
- 没有数据时直接插入
- 如果有数据,向左走,重复上述 1,2 步骤
public class Tree { public TreeNode root; @Override public String toString() { return "Tree{" + "root=" + root + '}'; } public void insert(int val){ TreeNode newNode = new TreeNode(val); if(root == null){ root = newNode; return; } TreeNode tempNode = root; while(true){ //插入值小于当前节点 while(newNode.value <= tempNode.value){ if(tempNode.left == null){ tempNode.left = newNode; return; }else{ tempNode = tempNode.left; } } //插入值大于当前节点值 while(newNode.value > tempNode.value){ if(tempNode.right == null){ tempNode.right = newNode; return; }else{ tempNode = tempNode.right; } } } }
完全二叉树的删除:
- 计算机中没有真正意义上的删除,只是把指针置空
- 步骤:
- 删除叶子节点:
- 找到想要删除的节点 targeNode
- 找删除的节点是否有父节点 parent(父节点是否存在)
- 要删除的节点是父节点的左子树还是右子树
- 如果左子树,则 parent.left = null
- 如果是右子树,则 parent.right = null
- 删除只有一个子节点的节点:
- 找到要删除的节点是否 targeNode
- 找删除的节点是否有父节点 parent(父节点是否存在)
- 确定该节点待删除的节点是有左子树还是右子树
- 要删除的节点是父节点的左子树还是右子树
- 如果删除的节点是父节点的左子树,且被删除的节点有左子树:
- parent.left = targeNode.lenft
- 如果删除的节点是父节点的左子树,且被删除的节点有右子树:
- parent.left = targeNode.right
- 如果删除的节点是父节点的右子树,且被删除的节点有左子树:
- parent.right = targeNode.left
- 如果删除的节点是父节点的右子树,且被删除的节点有右子树:
- parent.right = targenNode.right
- 删除有两个子树的节点:
- 找到要删除的节点 targeNode
- 找到要删除的节点的父节点 parent (考虑父节点是否存在)
- 从 targeNode 找到右子树最左侧的节点
- 或者左子树最右侧的节点
- 用临时变量 temp 记录该值
- 删除最小节点
- 将要删除的节点的值替换为 temp
//删除节点 public void delete(TreeNode node,int val){ //判空 if(node == null){ return; } //删除根节点 if(node.left == null && node.right == null){ node = null; return; } //查找删除节点 TreeNode targeNode = findTargeNode(node,val); if(targeNode == null){ System.out.println("没有该节点"); return; } //查找父节点 TreeNode parentNode = findParent(node,val); if(parentNode == null){ System.out.println("没有找到父节点"); } if(targeNode.left == null && targeNode.right == null){ //删除叶子节点 //父节点的左子节点 if(parentNode.left != null && parentNode.left.value == val){ parentNode.left = null; }else if(parentNode.right != null && parentNode.right.value == val){ //该节点为父节点的右子节点 parentNode.right = null; } } else if (targeNode.left != null && targeNode.right != null) { //待删除节点有两个子节点 int data = findRightMin(targeNode.right); targeNode.value = data; }else { //删除有一个子节点的节点 //当父节点的左节点是删除节点时 if(parentNode.left != null && parentNode.left.value == val){ //删除节点有左孩子 if(targeNode.left != null){ parentNode.left = targeNode.left; targeNode = null; }else { //有右孩子 parentNode.left = targeNode.right; targeNode = null; } } else { //父节点的右节点为删除节点 //删除节点有左孩子 if(targeNode.left != null){ parentNode.right = targeNode.left; targeNode = null; }else { //有右孩子 parentNode.right = targeNode.right; targeNode = null; } } } } }
- 删除叶子节点: