--- title: 数据结构和算法-未完成 author: TianZD top: true cover: true toc: true mathjax: false summary: 基于Java的数据结构和算法,未完成,粗略学了一下,没有参考价值 tags: - 数据结构 - 算法 categories: - java - 数据结构和算法 reprintPolicy: cc_by abbrlink: b1f89a38 date: 2022-04-29 10:42:56 coverImg: img: password: --- [TOC] > 概述:学习《漫画算法:小灰的算法之旅》 # 1、数据结构基础 ## 1.1、数据结构分类 >数据结构是以某种特定的布局方式可存储数据的容器,布局方式决定了数据结构对于某些操作是高效的,对于其他操作是低效的。 数据结构 = 逻辑结构+物理结构(顺序、链式、索引、散列) * 逻辑结构 * 线性结构:顺序表、栈、队列 * 非线性结构:树、图 * 物理结构:(存储结构),在计算机存储器中的存储形式 * 顺序存储结构:数组 * 链式存储结构:链表 逻辑分类: 1. 线性结构:结构中的元素存在一对一的相互关系(元素之间有序,一个挨一个),常见的有:线性表、栈、队列、串(一维数组); 2. 树形结构:元素存在一对多的相互关系;常见的有:二叉树、红黑树、B树、哈夫曼树等; 3. 图形结构:元素存在多对多的关系;常见:有向图、无向图、简单图; ## 1.2、数组 ### 概述 * 有限个相同类型的变量组成的有序集合 * 物理结构:**顺序存储结构**,在内存中顺序存储 * 逻辑结构:**线性结构** ### 基本操作 > 读取元素 可以直接通过下标进行随机读取,时间复杂度O(1) > 更新元素 通过下标直接进行赋值替换,时间复杂度O(1) > 插入元素 涉及到元素的移动,时间复杂度O(n) * 尾部插入 * 中间插入 * 超范围插入(扩容) > 删除元素 和插入相反,时间复杂度O(n) ### 优劣势 优点: * 数据访问:可以通过下标进行,效率高 缺点: * 插入删除:在内存中采用顺序存储,插入和删除导致大量元素移动,效率低 总结:适用于**读操作多、写操作少**的场景 ## 1.3、链表 ### 概述 * 分为单向链表和双向链表 * 物理上非连续、非顺序,在内存中**随机存储** * 由若干节点**node**组成 * 物理结构:**链式存储结构** * 逻辑结构:**线性结构** > 单向链表 每一个node包含两个属性: * data:存放数据的变量 * node节点指针next:指向下一个节点 ```java private static class Node{ int data; Node next; } ``` 单向链表只能通过next指针找到关联的下一个节点,逐一寻找,只需要知道第一个节点,便可以根据找到所有的节点,因此用第一个节点标识链表,单向链表的第一个节点又称为**头节点** 头尾节点: * 头节点Head:链表的第一个节点 * 尾节点:链表的最后一个节点,next指针指向空,null > 双向链表 双向链表每一个node包含三个属性: * data * node节点指针next * **node节点指针prev:指向前一个节点** ### 基本操作 > 查找节点 从头节点开始,向后逐一查找,时间复杂度O(n) > 更新节点 找到后直接将node节点数据进行替换,查找时间复杂度O(n),替换复杂度O(1) > 插入节点 如果不考虑查找元素的过程,时间复杂度O(1) * 尾部插入 将尾节点的next指针指向新节点 * 头部插入 1.把新节点的next指向原来的头节点 2.把新节点变为头节点 * 中间插入 1.将新节点的next指向插入位置的节点 2.将插入位置前的节点的next指向新节点 > 删除元素 如果不考虑查找元素的过程,时间复杂度O(1) * 尾部删除 把倒数第二个node的next指向空 * 头部删除 把链表的头节点设为原来的第二个节点 * 中间删除 把要删除节点的前一个节点的next指向后一个节点 ### 优劣势 优势: * 在于能够灵活地进行插入和删除操作 劣势: * 查找需要从头部开始逐一遍历 ## 1.4、栈 ### 概述 * 逻辑结构:**线性存储结构** * 物理结构:可以用数组实现也可以用链表 * 先入后出FILO * 栈顶:最后进入元素的位置 * 栈底:最先进入元素的位置 ### 基本操作 > 入栈push 把新元素放入栈中,只能从栈顶放入,作为新的栈顶 时间复杂度O(1) > 出栈pop 把元素弹出,只能弹出栈顶元素 时间复杂度O(1) ### 应用 输出顺序和输入顺序相反,通常用于对“历史”的回溯 * 代替递归 * 面包屑导航(浏览页面) ## 1.5、队列 ### 概述 * 逻辑结构:**线性存储结构** * 物理结构:可以用数组实现也可以用链表 * 先入先出FIFO * 对头front * 对尾rear ### 基本操作 > 入队enqueue 把新元素放入队尾 时间复杂度O(1) > 出队dequeue 把对头元素移出队列 时间复杂度O(1) > 循环队列 数组实现的队列为了防止出队时,对头左边的空间失去作用,可以采用循环队列的方式维持队列容量的恒定 * 入队: * 队列满:`(rear+1)%array.length==front` * `rear=(rear+1)%array.length;` * 出队: * 队列空:`rear==front` * `front=(front+1)%array.length;` ### 应用 输出顺序和输入顺序相同,用于对历史的回放 * 多线程中争夺锁 * 网络爬虫抓取 ## 1.6、散列表(哈希表) ### 概述 * 散列表提供了**键(key)** 和**值(value)**的映射关系,可以根据key快速找到对应的value,时间复杂度接近O(1) * 本质上是**数组** * **通过哈希函数将key转换为数组中的下标** ### 哈希函数 > hashcode 在java等面向对象的语言中,每一个对象都有自己的**hashcode**,hashcode是区分不同对象的重要标识,不论对象的类型是什么,hashcode都是一个**整型变量** > 转化 通常采用hashcode对数组长度的**取模运算** `index = HashCode(key) % Array.length` java中的哈希函数采用的是**位运算**的方式,将数组长度取为2的次方,提高性能。 ### 基本操作 > 写操作 1. 通过哈希函数,将key转化为数组下标 2. 放入元素 1. 如果对应的下标没有元素,直接放入 2. 如果已有元素,产生**哈希冲突** 哈希冲突解决: * **开放寻址法**:已有元素时,`index++`,放入下一个,直至对应的下标没有元素占用,ThreadLocal采用 * **链表法**:**HashMap采用**,其数组中的每一个元素对应链表的一个节点,最先放入的为头节点,后续放入的通过将上一个节点的next指向该元素,插入到链表中 > 读操作 1. 通过哈希函数,将key转化为数组下标 2. 找到对应下标对应的元素 1. 如果只有一个,则找到 2. 如果对应的链表,遍历链表,**直到key值对应** > 扩容 当多次插入后,key映射位置发生冲突的概率提高,大量元素拥挤在相同的位置,形成链表很长,导致查询和写入效率低下 条件: `HashMap.Size >= Capacity * LoadFactor` **步骤**: * **扩容**:新建一个空数组,长度为两倍 * **重新Hash**:数组长度变化,需要重新根据哈希函数计算哈希值 ## 1.7、树和二叉树 ### 概述 > 树 * 逻辑结构:非线性结构 > 二叉树 ### 二叉树应用 > 查找 > 维持相对顺序 ### 二叉树遍历 从节点位置: * **前序遍历** * **中序遍历** * **后序遍历** * **层序遍历** 从宏观角度: * **深度优先遍历:前序、中序、后序** * **广度优先遍历:层序** > 深度优先遍历 > 广度优先遍历 ## 1.8、二叉堆 ### 概述 * 最大堆 * 最小堆 ### 自我调整 > 插入 > 删除 > 构建 > 代码 ## 1.9、优先队列 ### 概述 ### 实现 # 2、排序算法 ## 2.1、分类 ## 2.2、冒泡排序 ### 基本冒泡排序 交换排序,两两比较,两层循环,第一层为遍历的轮数:`array.length-1` 时间复杂度O(n^2) ### 优化冒泡排序 > 第一轮 防止数列已经有序后仍然进行下一轮排序 如{5,8,6,3,9,2,1,7} > 第二轮 现有逻辑:从`for(int j=0;j 双边循环法 * 基准元素pivot * 左指针left * 右指针right > 单边循环法 * 基准元素pivot * 元素区域边界mark ### 递归、非递归实现 ## 2.4、堆排序 ## 2.5、计数排序和桶排序 ## 2.6、排序算法复杂度分析 | 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 是否稳定 | | ---------- | -------------- | -------------- | ---------- | -------- | | 冒泡排序 | O(n^2) | O(n^2) | O(1) | 稳定 | | 鸡尾酒排序 | O(n^2) | O(n^2) | O(1) | 稳定 | | 快速排序 | O(nlogn) | O(n^2) | O(logn) | 不稳定 | | 堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 | | 计数排序 | O(n+m) | O(n+m) | O(m) | 稳定 | | 桶排序 | O(n) | O(nlogn) | O(n) | 稳定 | # 3、面试算法 ## 3.1、判断链表是否有环 ### 基础版 > 问题 如何判断一个链表是否有环 > 思路 链表若有环,两个指针分别不同速度,速度快的会追上速度慢的 * 两个指针指向头节点:`Node p1 = head; Node p2 = head;` * p1每次移动一个位置:`p1=p1.next;` * p2每次移动两个位置`p2=p2.next.next;` * p1和p2相遇则有环:`p1==p2` > 代码 ### 扩展 > 问题 若有环,求环的长度和入环节点 > 思路 **环的长度**:p1和p2的速度相差1,因此二次相遇之间走过的步数就是环的长度 **入环节点**:通过数学计算可以得到,入环点到头节点的距离==首次相遇点到入环点之间的距离,因此首次相遇后,把一个指针重新指向头节点,两个指针同时一次走一步,再次相遇的点就是入环节点 ## 3.2、最小栈 > 问题 实现一个栈,有出栈、入栈,另外要有取最小值方法,同时三个方法的时间复杂度都为O(1) > 思路 > 代码 ## 3.3、求最大公约数 > 问题 求两个整数的最大公约数 > 思路 **辗转相除法(欧几里得算法)**:两个正整数a和b(a>b)的最大公约数等于a除以b的余数c和b之间的最大公约数 **更相减损法**:两个正整数a和b(a>b)的最大公约数等于a-b的差值c和b之间的最大公约数 > 代码 > 优化 位运算 ## 3.4、判断一个数是否是2的整数次幂 > 问题 判断一个数是否是2的整数次幂 > 思路 * 2的整数次幂装换位二进制时,首位为1,其余位为0 * 2的整数次幂减去1,转换为二进制时,全部为1,首位为0 * 与运算& > 代码 ## 3.5、无序数组排序后的最大相邻差 > 问题 求出数组排序后的任意两个相邻元素的最大差值 > 思路 采用桶排序的思想, * 分桶 * 入桶,记录每一个桶的最大值和最小值 * 统计相邻两个桶的最大值和最小值的差 > 代码 ## 3.6、用栈实现队列 > 问题 用栈模拟队列,实现:入队、出队 > 思路 * 两个栈A和B * 入队在A中压栈 * 出队 * B是否为空,空的话首先将A中的元素弹出到B中 * B弹栈 > 代码 ## 3.7、寻找全排列的下一个数 > 问题 给一个正整数,找出全排列的下一个数 用这个数包含的全部数字重新排列,找出一个大于且仅大于原数的新整数 > 思路 固定的几个数字进行排列,逆序排列时数字最大,顺序排列时数字最小,同时为了保持重新排列的数仅比原数大,要尽可能保持高位不变,仅仅改变低位的数字 * 从后找,找到当前整数的逆序区域(逆序区域已经是最大的了,要找到不是逆序区域的数字的位置),如12354,只变换5和4不能满足比原数大,要从3开始变 * 更换找到的位置的数字和最末尾数字,变为12435 * 把逆序区域变为顺序,即交换5和4,保持最小 > 代码 ## 3.9、删除k个数字后的最小值 > 问题 给一个整数,删除k个数字,要求剩下的数字尽可能小 > 思路 删除掉一个数字后,整数的位数减少1 需要从高位起,若该位置数字比后一位大,则需要去掉 > 代码 ## 3.9、实现大整数相加 > 问题 实现两个很大的整数相加 > 思路 根据手动相加思路,低位相加,大于10后,高位加1 * 数字逆序存到两个数组中 * 遍历,若两个数组对应的位置值相加大于10,则保留个位,且高位加1 * 下一位相加,同时要加上低位进的1 > 代码 ## 3.10、金矿问题 > 问题 10名工人,5堆金矿,价值{500,400,350,300,200},需要人力{5,5,3,4,3} 求解如何最大收益 > 思路 动态规划 > 代码 ## 3.11、寻找缺失的整数 ### 基本问题 > 问题 一个无序数组,有99个不重复的正整数,范围1-100,求出100中缺少的那个 > 思路 先计算累加和,然后减去有的99个数,得到的即为缺少的 ### 扩展1 > 问题 数量若干,同时99个整数出现了偶数次,1个出现了奇数次,求出这一个数 > 思路 异或运算 ### 扩展2 > 问题 2个数字出现了奇数次,求这两个数 > 思路 分治法 # 4、算法的应用 ## 4.1、Bitmap ## 4.2、LRU ## 4.3、A星寻路算法 ## 4.4、红包算法