14 KiB
title | author | top | cover | toc | mathjax | summary | tags | categories | reprintPolicy | abbrlink | date | coverImg | img | password |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
数据结构和算法-未完成 | TianZD | true | true | true | false | 基于Java的数据结构和算法,未完成,粗略学了一下,没有参考价值 | [数据结构 算法] | [java 数据结构和算法] | cc_by | b1f89a38 | 2022-04-29 10:42:56 | <nil> | <nil> | <nil> |
[TOC]
概述:学习《漫画算法:小灰的算法之旅》
1、数据结构基础
1.1、数据结构分类
数据结构是以某种特定的布局方式可存储数据的容器,布局方式决定了数据结构对于某些操作是高效的,对于其他操作是低效的。
数据结构 = 逻辑结构+物理结构(顺序、链式、索引、散列)
-
逻辑结构
- 线性结构:顺序表、栈、队列
- 非线性结构:树、图
-
物理结构:(存储结构),在计算机存储器中的存储形式
- 顺序存储结构:数组
- 链式存储结构:链表
逻辑分类:
- 线性结构:结构中的元素存在一对一的相互关系(元素之间有序,一个挨一个),常见的有:线性表、栈、队列、串(一维数组);
- 树形结构:元素存在一对多的相互关系;常见的有:二叉树、红黑树、B树、哈夫曼树等;
- 图形结构:元素存在多对多的关系;常见:有向图、无向图、简单图;
1.2、数组
概述
- 有限个相同类型的变量组成的有序集合
- 物理结构:顺序存储结构,在内存中顺序存储
- 逻辑结构:线性结构
基本操作
读取元素
可以直接通过下标进行随机读取,时间复杂度O(1)
更新元素
通过下标直接进行赋值替换,时间复杂度O(1)
插入元素
涉及到元素的移动,时间复杂度O(n)
-
尾部插入
-
中间插入
-
超范围插入(扩容)
删除元素
和插入相反,时间复杂度O(n)
优劣势
优点:
- 数据访问:可以通过下标进行,效率高
缺点:
- 插入删除:在内存中采用顺序存储,插入和删除导致大量元素移动,效率低
总结:适用于读操作多、写操作少的场景
1.3、链表
概述
- 分为单向链表和双向链表
- 物理上非连续、非顺序,在内存中随机存储
- 由若干节点node组成
- 物理结构:链式存储结构
- 逻辑结构:线性结构
单向链表
每一个node包含两个属性:
- data:存放数据的变量
- node节点指针next:指向下一个节点
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的次方,提高性能。
基本操作
写操作
- 通过哈希函数,将key转化为数组下标
- 放入元素
- 如果对应的下标没有元素,直接放入
- 如果已有元素,产生哈希冲突
哈希冲突解决:
- 开放寻址法:已有元素时,
index++
,放入下一个,直至对应的下标没有元素占用,ThreadLocal采用 - 链表法:HashMap采用,其数组中的每一个元素对应链表的一个节点,最先放入的为头节点,后续放入的通过将上一个节点的next指向该元素,插入到链表中
读操作
- 通过哈希函数,将key转化为数组下标
- 找到对应下标对应的元素
- 如果只有一个,则找到
- 如果对应的链表,遍历链表,直到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<array.length-1;i++)
可以看出有序区的长度为以进行排序的轮数i
当后面大部分已经有序后,仍然会进行比较,对此进行优化
如{3,4,2,1,5,6,7,8}
鸡尾酒排序
双向交换,奇数轮从左向右,偶数轮从右向左
解决大部分已经有序的情况,如{2,3,4,5,6,7,8,1}
2.3、快速排序
概述
核心:分治思想
选定基准元素,大的放右边,小的放左边,一分为2
时间复杂度O(nlogn)
基准元素选择
- 默认数列的第一个元素
- 可以随机选择一个元素作为基准,然后和首元素交换位置
元素交换
双边循环法
- 基准元素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每次移动一个位置:
- 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个数字出现了奇数次,求这两个数
思路
分治法