左 . 算法---排序 /栈队列/链表 专题

排序:

冒泡排序:  时间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   两个相交直接是环





代码:







     

猜你喜欢

转载自blog.csdn.net/duoduo18up/article/details/80465156