排序:
冒泡排序: 时间O(n2) 额外空间O(1)
选择排序 时间0(n2)
原理就是 :在 0-n-1中找最小 -----放在位置0上
在1--n-1中找最小 -------放在位置1上 ......
总结:
选择排序和冒泡排序的排序方法与数据的状况无关 无论哪种情况总是O(n2).
插入排序:时间O(n2)
(类似于打扑克牌 ,你要把新搬入的牌插入到已有的牌中)
原理:
----------------i
------j ( i-1)------i (j+1 ) 即如果前大后小 则二者交换 刚好就是j 和j+1位置上的元素进行交换
总结:
插入排序 时间复杂度与 具体数据的情况有关:
最好情况: 数组已经有序-----只需要扫描一遍即可 O(n)
最坏情况/平均情况: 均需要扫描交换 -------O(n2)
在估计算法好坏时 是按照最坏情况下计算的时间复杂度指标。
对数器的准备(样本测试的工具):
以冒泡排序为例:
其中copyArray操作是新建另一个新的数组 ,然后将要复制的原数组内容 复制到此数组中
递归
求出一个数组的最大值:
归并排序:
时间复杂度:
归并排序涉及到的相关问题:
小和问题和逆序对问题
总结:
为什么 归并排序 会 比 一般的冒泡XXX都快?
因为避免了无用的比较。
e.g. 冒泡 :扫描了全体 就排好一个数字
而 归并没有浪费 已经排好的小部分就是有序的 只需要进行已经排好的小部分之间的外排(就是两个指针比较)
快速排序:
对于小的 那端 cur++ 是因为已经考察过 所以直接跳过即可
而对于大的那端 交换的数字是待定区域 所以还需要考察
经典快排的改进(按照荷兰国旗问题思路)--------以及随机快排::
总结:
经典快排一次只能是搞定一个数 不管左边是否有X数 依然进行递归 而改进版的 只要是等于X的数无需再动
经典快排的问题:
容易“搞偏” 每次只搞定一个 时间复杂度和数据状况有关 可能退化成O(n2)的算法
而选取基准刚好在中间 则时间复杂度是最好的O(nlogn)
最终使用 随机快排
随机选取一个数----跟最后一个数进行交换---概率事件--即使出现偏离事件--但是最终的时间复杂度也是可以接受的
代码: 就是上面加上随机数选取那句代码。
拓展: 想要绕开已有的数据状况 我们怎么做:
2种方式: 1 类似上面 随机选取
2 采用哈希函数进行打乱
随机快排的时间复杂度和空间复杂度:
最好的是O(logn) 因为每回需要去记录断点位置 以便分为左右两部分----总共需要O(logn)
最坏的是O(n) 因为每回的基准均是在单侧 -------导致每次都需要记录----总共需要O(n)
无论数据情况如何 长期期望就是O(nlogn)
堆:
完全二叉树及 节点之间的对应关系:
根据 数组如何变成具体的大根堆结构:
1 建立大根堆的 时间复杂度:
对应的是heapInsert 函数 -----即加入一个新节点并向上进行调整的过程
只跟其上的父节点有关 是完全二叉树的深度
每次加入一个节点都是 ---O(logn)
即 O(log1)+O(logn2)+......O(logn)= O(N) 总的就是O(N)
2 场景(heapify): 如果一个大根堆其中的某一个节点 值突然发生变化,需要重新进行调整 ----
------------找到其两个子孩子---取其中子孩子的最大值,如果比现在的这个节点值大----则与之交换-----不断向下调整交换----
heapsize 参数标记 堆目前的大小 <=数组的大小
3 堆上如果要把根节点弹出 则用最后一个节点和根节点互换-----之后通过heapify向下调整--------大小-1
堆排序原理:
就是每次将根节点和最后一个节点进行交换 -----然后原根节点固定在数组最后并失效----新的节点通过heapify进行调整----
----继续得到根节点---继续和最后一个交换------不断重复上述操作------最终即可得到排序的结果
整个的过程:
比较各种排序算法的稳定性:
稳定性: 相同元素 在排序前后的相对位置不会发生变化
冒泡排序: 只有大或小时才进行交换 相等就跳过 -------稳定!
插入排序: 在找寻位置的时候 ,遇到大或小会交换 ,但相等时就不会有操作,依然保持原有顺序------稳定!
选择排序: 一轮下来的极值与第0位交换----可能跨过好多个相等的元素---导致次序被打乱------不稳定!!
归并排序: 因为分别比较并拷贝两个子集合中的元素----规定先拷贝左边或右边---则陆续拷贝不破坏顺序-----稳定!
快速排序: 因为partiton 函数就无法保证稳定性-------不稳定!!
堆排序: 也是不稳定的! 不会注意相等值的!
荷兰国旗问题: 不稳定!!444 3 交换了3和第一个4 次序已经发生变化
稳定性的意义:
在实际应用中 我们追求稳定性 :是因为之前的记录可能对我们也有着重要的作用,我们希望在新排序之后依然能够保留原有记录。
例子: 学生---班级号----成绩
先 按照成绩进行排序 如图所示
后 按照班级号进行排序:
结果是在1班 里的王和李 前后顺序同上次,成绩依然按从小到大进行排序 ,在可读性方面较好 ,不乱很清晰。
有关排序问题的补充:
实际工程中使用的排序算法:
在数组长度非常短(一般<60) : 用插排 -----因为数据量少---O(n2)不大 且常数项很小----速度很快
若数组中存储的是基本数据类型: 用 快排 因为基础类型 不用保证稳定性!
若数组中存储的是自己定义的class ,使用某一个字段进行排序: 用 归并排序 因为类似于上面班级成绩的例子,需要保证稳定性!
当数据量较大 ,用归并或者快排 递归 ,当分解之后数据<60 ,其实里面已经使用了插排 ----所以这是一种综合~~
比较器的使用:
compare 函数中:
return 负数 :第一个放在前面---第二个放在后面
正数: 2 1
0 :相等
在实际程序的书写中,如果排序不是重点时, 则直接书写自己的比较器并调用系统排序即可。
堆也可以有比较器:
如果不定义自己的比较器 就会按照自然顺序比较,但对于student类 ,不知如何比较,会报错。
不进行比较的排序:
桶排序 :
类似于定义一些容器 将同类的东西放入自己归属的桶 不进行比较 在O(n)时间内 可分类完毕(有点像 数组元素 统计频数)
应用题目:
分析题意:
给定数组中N个元素----准备N+1个桶---数组中的max/min分别放在第一个和最后一个桶 ----其余按照等份去放入
则相邻元素之间的最大差 一定出现在空桶左右相邻桶 左边的最大值和右边的最小值满足相邻且可能差值最大。
实际操作:
需要 boolean[N+1]数组记录是否有数入桶
max[N+1] min[N+1] 记录每个桶里面的最大值和最小值
代码:
//循环一遍---找出其中的最大值和最小值-----判断是否相等
1 相等 ----则数组中只有一种数
2 不等-----则继续分桶
//创建3个辅助数组 并循环原始数组 将其中数字分在各个桶里
//之后 每个桶里 根据新分入的数字进行max/min更新---并记录boolean 为true(桶里有数字)
//分桶完毕--辅助变量记录上一次的最大值lastMax---循环每个桶---不断将上一次最大值和下一个桶的最小值进行差值--比较 ----直到循环桶完毕---res记录了最大差值 ----即为结果。
队列和栈
用数组结构实现大小固定的队列和栈:
实现固定大小的栈:(重新创建一个新的数组)
实现固定大小的队列:(重新创建一个新的数组)
主要有3个指针变量:
size end start
其中 end: 每次用户加入新值的时候 如果size未满 则 加到end指的位置上
start: 每次用户想要取出元素的时候 只要size!=0 就取start位置上的数字给用户
size : 每次记录已有数据量
在整个数组的运行过程中 我们可以看出:数组类似于一个循环 end 到底之后就回到开头
代码:
getmax函数的设计:
第一种:两个栈----同步进行压入----始终比较数据栈和min栈顶元素值的大小
用队列实现栈 (用栈实现队列):
用队列实现栈:
代码:
用栈实现队列:
具体代码见链接:
import java.util.Stack; public class Solution { Stack<Integer> stack1 = new Stack<Integer>(); Stack<Integer> stack2 = new Stack<Integer>(); //队列的压入就是s1的压入数据 public void push(int node) { stack1.push(node); } //弹出借助于S1/s2 两个 弹出需要考虑队列是否为空 public int pop() { if(size()!=0){ if(stack2.empty()){ //若s2为空 则需要把s1的元素全部压入s2再输出 while(!stack1.empty()) stack2.push(stack1.pop()); } return stack2.pop(); //若s2不为空 则直接输出 }else{ System.out.println("队列为空,不能执行出队操作"); return -1; } } //队列的大小 队列为空则说明 s1/s2均为空 public int size(){ return stack1.size()+stack2.size(); } }
猫狗队列(工程类型题目):
思路:
两个队列,一个cat队列,一个dog 队列 分别进出 ,按照正常队列
但是 对于要pollAll ,则需要进行标记 ,对于cat /dog的次序进行记录
则 想到 新建一个petEnter类 ,里面含有pet 和 count 属性 ,作为一个整体的宠物
在 之后add队列的时候对每一个宠物进行count标记 最终pollAll时比较两者count大小,即说明次序
已有程序结构:
我们书写的程序结构:
队列:
转圈打印矩阵:
不同于剑指offer思路:
这里主要使用左上角一个点 和 右下角一个点 去整体控制
下面代码中4个while循环:
代码:
顺时针旋转矩阵90度:
有一个NxN整数矩阵,请编写一个算法,将矩阵顺时针旋转90度。
给定一个NxN的矩阵,和矩阵的阶数N,请返回旋转后的NxN矩阵,保证N小于等于300。
借助刚才的思路:
分清楚哪些点是一组的 从而进行相应的操作
优点: 不用辅助数组-----直接原地进行操作
代码:
之字形打印矩阵:
思路:
从第一个点开始设置A ,B ,其中 A 不断向右走---->遇到末尾就向下
B 不断向下走---->遇到末尾就向右
可以发现 两个点之间连成的对角线-----刚好就是之字形打印的顺序
问题转化为------>如何打印出A和B形成的对角线问题~并且用boolean变量标记打印顺序(斜向上或者斜向下)
代码:
while条件的理解: A已经向右走完 并且 也已经向下走完 ----走到最后一行
与B走到最后一列同理~
boolean fromup: 为false时 打印的是从下到上 即开始的打印1
后来变成true时 打印的是从上到下 即打印2 5 .。。。。之后以此类推 即可
数组问题
在行列均排好序的矩阵中找数:
思路如下:
代码:
private static boolean arrSearch(int[][] arr, int key) { if(key<0) return false; if(arr==null) return false; int i=0; //行数row int j=arr[0].length-1; //列数column boolean flag=false; //查找标志 while(i<arr.length && j>=0) { //从右上角开始查找 if(key==arr[i][j]) { flag=true; break; } else if(key<arr[i][j]) j--; else i++; } return flag; } }
链表问题
打印两个有序链表的公共部分:
思路:
因为已经是有序链表---所以采用类似于“外排”的思路-----比对两者大小---
小者先行----直到遇到相同数值的节点---开始打印-----直到链表结束
代码:
判断一个链表是否是回文结构:
思路1 :用栈 一遍遍历链表 压入栈 --- 栈弹出----再和原链表一步步比对---
若有一个不满足相等条件--则 返回false 对比长度为全部长度
思路2: 先使用快慢指针找到链表中点位置-----然后将慢指针之后的元素压入栈中----最后弹栈-----
快指针重新回到链表开头---继续进行对比 对比长度为一半长度
以上两种思路 均需要额外的辅助空间
下面的思路(不需要额外的辅助空间):
1 快慢指针 ---找到链表中点位置
2 将慢指针指向的链表进行反转
3 分别从两个分链表的头部进行比对 若出现不等 则返回false 否则 返回true
但一定记得在 题目做完 之后 记得把后半部分逆序的链表再调整回来---恢复原来的状态---做题不能破坏原有结构。
细节点:
奇数时: 就是中点位置
偶数时: 要取两个中间位置的前一个节点
思路1 (笔试中适合做的): 直接将链表所有节点放入数组中 -----荷兰国旗问题处理------最后将数组中的节点值依次串起即可
额外辅助空间为O(n)
思路: 既要保持稳定性 又要降低辅助空间
定义3个Node节点 ---- 名称为less more equal -----一次遍历之后分别获得第一个less more equal----之后继续遍历-----分别接在这三者的后面 ---- 统计每一个分链表的end指针-----最后将这三个分链表通过end连接------再连到一起
复杂链表的复制问题(复制含有随机指针节点的链表)
思路1 :使用“哈希表”的思想,存节点1 和1‘ 对应。。。。在遍历过程中对应重建一个新的链表
思路2 :不用额外辅助空间的解法 每一个节点的下一节点均是其拷贝节点-----
代码:
两个单链表相交的一系列问题:
扩展出 3个问题:
1 判断单链表是否有环?
2 两个无环的单链表的第一个交点?
3 两个有环的单链表的第一个交点?
对应本题: 给定两个单链表的头节点 ,如果两个链表相交 ,请返回相交的第一个节点 ,如果不相交,返回null。
思路1 : 最简单想法: 用hashset 不用存value 只存key值 就是节点值
若发现有重复则 有环 且重复的第一个节点即为环的第一个相交节点。 若发现出现null 则证明无环 。
思路2 : 用快慢指针----快指针一次走两步 慢指针一次走一步 两者肯定会在环上相遇 ----若快指针为null则证明无环
后快指针回到链表开头---和慢指针同步,每次均走一步----则两者一定会在环入口节点处相遇。
若两个单链表都没有环 则第一个节点?
1 用map 把第一个链表的的节点遍历存入---再遍历第二个链表------重复的第一个节点即为所求。
2 分别遍历两个链表 并记录最后一个节点和长度 若最后一个节点相同则 证明有交点,长的那个链表先走(l-s)步 之后两个一起走~
若一个链表有环 一个链表无环 则不可能相交
若两个链表都有环 求相交的第一个节点?
拓扑结构有以下3种:
定义两个链表 head 1 head2 loop1 loop2
loop1=loop2 为情况2
loop1!=loop2 为情况1 和 3 ------- 如何区分?
1 让loop1 继续走下去 始终遇不到loop2 ------情况1
2 让loop1 继续走下去遇到loop2 -------------情况2
1 两个压根不相交
2 两个相交先直后环
可以看作无环链表相交的问题 ---第一个交点和环无关 -----利用 长度差方法即可求解
3 两个相交直接是环
代码: