【篇首随笔】:
自从听了左神的课,每一节课我都得耗费20张A4的大草纸,先别说有没有全听懂,但起码我好像听进去了(起码比其他高播放量但按部就班背PPT的课好上N倍),纵观全网,好像暂时没有发现其他讲师讲的比左神清晰明了,记笔记的方式又回到了《刀耕火种》模式,是的,拿着中性笔在A4草纸上边听边话,想起了以前听宇哥的数学课,也想起了第一次学Java编程时候的记笔记方式。
自从听了左神的课,我头一次意识到,原来算法是如此的重要,算法是如此的灵活,据说左神当年在国外留学的时候,听一节算法课要交600元一小时 , 又听说左神为了出算法书从国内某知名企业辞职。
就这样我在B站一分钱都没交,竟然白嫖到了让我豁然开朗的课,内心只有一个声音——左神YYDS!!!
另外宣传一下左神的算法书(听了两节课之后秒下单)——《程序员代码面试指南》。
另外左神有一个非常令人崇拜的地方,在讲算法的时候动不动就拖出几个数学公式,彻彻底底的从数学原理出发来讲,给人的感觉就是我没有刻意的备课,数学已经融入了这个人的生命,怎么说呢,就是有文化的人说话并不是刻意地去组织语言,而是因为常年的读书生涯已经将这个人的思维方式说话方式给固化了,引经据典脱口而出的赶脚。
还有一点,就是他讲课的时候,设计到算法的部分,全程用手拿着pencil在电子屏幕上画,大学的时候学数学的时候,牛啤的数学老师都是这么一个讲法,最后感谢左神的无私奉献(其实一开始是奔着标题去的——《两周刷爆LeetCode》),起初奔着标题去,而现在我的心态就是太令人疯狂了,算法完全就是一个暴露人智商的科目,怪不得国外只考这个玩意儿。
【一些概念】:
【master公式】:
- 估计一系列特殊的递归行为时,可以用这个公式;
T(N)————母问题的数据量为N 。
=号右边————在函数里面的细节。
T(N/b)————子问题的规模都是(N/b)规模的子问题。(调用子问题的时候,规模是不是等量的,是否都是 N/b 规模)
a————这个子问题被调用了多少次(调用次数)。
【递归数组最大值】:
public class Code02_递归数组最大值 {
public static int getMax(int[] arr){
return process(arr, 0 ,arr.length-1);
}
// arr[L...R] 范围上求最大值 N
private static int process(int[] arr, int L, int R) { //process就是母问题;
if (L==R){ // arr[L...R] 范围上只有一个数,直接返回,base case .
return arr[L];
}
int mid = L + ((R-L)>>1 ); //中点。
int leftMax = process(arr, L, mid); //这一句的规模是 N/2 ( L~mid 的规模是N的一半 )
int rightMax = process(arr, mid+1, R);
return Math.max(leftMax, rightMax);
}
}
【不符合master公式的情况】:
【切三段然后遍历】:
【公式的具体使用】:
【 递归的实质 】:
在学C语言的时候,就接触递归了,但是一直没想过递归和栈,树的关系;
利用栈玩了一个后序遍历;每一个子节点都需要利用自己的子节点返回信息之后,才能往上返回,我的栈空间就是我整颗树的高度,我只用在一个高度上压栈即可,这就是所谓的递归过程。
【归并排序——MergeSort】:(墨汁墨汁)
墨汁的时间复杂度一下降到了 NlogN ! ! ! 之前认识了很久的冒泡 , 选择 , 插入都因为等差数列的问题挂了;
墨汁一下~
import java.util.Arrays;
public class Code02_MergeSort {
public static void mergeSort( int[] arr ){
if ( arr==null || arr.length<2 ){
return;
}
process(arr, 0, arr.length-1);
}
public static void process(int[] arr, int L,int R){
if ( L==R ){
return; //只有一个数的话,你还排什么序呢?一个数已经有序了!!!
}
int mid = L + ((R-L)>>1);
process(arr, L, mid); //左侧有序。
process(arr, mid+1, R); //右侧有序。
merge(arr,L,mid,R); //merge一下。
}
//merge过程没有调用任何递归行为;
public static void merge(int[] arr,int L, int M, int R){
int[] help = new int[R-L+1];
int i = 0; //help数组下标是从0开始的。
int p1 = L; //左侧的指针;
int p2 = M + 1; //右侧的指针;
while ( p1 <= M && p2 <= R ){ //有一侧的指针越界立即结束循环
//在不越界的情况下,两个指针所指向的值,谁小,把谁放到help[]中,并移动指针~
// i 是 help[] 的指针;
// p1 是左半边数组的指针;
// p2 是右半边数组的指针;
help[i++] = arr[p1] <= arr[p2]? arr[p1++]: arr[p2++];
}
//右侧提前越界
while ( p1 <= M ) { //进入这个循环说明————右侧的指针走得快
help[i++] = arr[p1++];
}
//左侧提前越界
while ( p2<=R ){ //进入这个循环说明————左侧的指针走得快
help[i++] = arr[p2++];
}
//下面是把原数组的元素全部更新一遍~~~
for (int j = 0; j < help.length; j++) {
arr[L+j] = help[j];
}
}
public static void main(String[] args) {
int[] a={9,5,7,6,1,2,3,4};
mergeSort(a);
Arrays.stream(a).forEach(value -> System.out.print(" "+value));
}
}
感觉手稿才是理解算法(数学)的关键~宇哥曾说:“我做梦都想拿到那个谁谁的手稿(展览在某个博物馆里的)”。
【数组小和问题】:
//小和问题中利用了右侧数组的有序性,因为可以通过下标计算出有多少个数比左指针大;
还有就是左右相等的情况,必须先考呗右边的,不然无法记数;
import java.util.Arrays;
public class Code2_小和Marge {
public static int smallSort( int[] arr ){
if ( arr == null || arr.length<2 ){
return 0;
}
return process(arr, 0, arr.length-1);
}
//arr[L...R] 既要排好序,也要求小和
public static int process (int[] arr , int l ,int r){
if (l==r){
return 0;
}
int mid = l + ( (r-l)>>1 );
return process(arr, l, mid)
+ process(arr, mid+1, r)
+ merge(arr,l,mid,r);
}
public static int merge(int[] arr , int L ,int m , int r){
int[] help = new int[ r - L + 1 ];
int i = 0;
int p1 = L;
int p2 = m + 1;
int res = 0;
// 都不越界的时候;
while ( p1 <= m && p2 <= r ){
res += arr[p1] < arr[p2] ? (r-p2+1)*arr[p1] :0;
//三目运算,不成立的话就加 0。
help[i++] = arr[p1] < arr[p2] ?arr[p1++] : arr[p2++]; //右组小于等于左组的时候,拷贝右组,否则就先左;
}
while ( p1 <= m ){
help[i++] = arr[p1++];
}
while ( p2 <= r ){
help[i++] = arr[p2++];
}
for (int j = 0; j < help.length; j++) {
arr[L+j] = help[j];
}
return res;
}
public static void main(String[] args) {
int[] a={1,4,3,2,7,5,3};
System.out.println(smallSort(a));
Arrays.stream(a).forEach(value -> System.out.print(" "+value));
}
}
【逆序对】:
墨汁算法的扩展,左神并未给出答案,下为自研;
public class Code2_逆序对 {
public static void main(String[] args) {
int[] arr = {9,3,2,4,5,0};
startNiXu(arr);
}
//定义一个方法,传入数组,立即打印出所有逆序对
public static void startNiXu(int[] arr){
process( arr, 0, arr.length-1 );
}
//arr[L...R] 既要排好序,也要求小和
public static void process (int[] arr , int L , int R){
if ( L==R ){
return ;
}
int mid = L + ( (R-L) >> 1 ); //中点
process(arr, L, mid); //左侧有序。
process(arr, mid+1, R); //右侧有序。
merge(arr,L,mid,R); //merge一下。
}
public static void merge(int[] arr , int L ,int m , int r){
int[] help = new int[ r - L + 1 ];
int i = 0; //help[] 指针
int p1 = L; //左指针
int p2 = m + 1; //右指针
// 都不越界的时候;
while ( p1 <= m && p2 <= r ){
if ( arr[p1] > arr[p2] ){ //左侧数字大,这个时候需要记录
for (int k = p1 ; k <= m ; k++) {
System.out.println("{"+arr[ k ]+" , "+arr[ p2 ]+"}");
}
help[i++] = arr[p2++];
}else {
help[i++] = arr[p1++];
}
}
//一旦结束这个循环必定是两种情况的一种发生;
//左侧率先越界的时候;
while ( p2 <= r ){
help[i++] = arr[p2++]; //把右侧数组剩余的填充进去;
}
//右侧率先越界的时候;
while ( p1 <= m ){
help[i++] = arr[p1++]; //把左侧数组剩余的填充进去;
}
//将help[] 逐一替换掉原数组的元素;
for (int j = 0; j < help.length; j++) {
arr[L+j] = help[j];
}
}
}
【下为图解】:
左神在视频中说:“这种墨汁问题,每年都会考的,每年都会有变形”。
【 荷兰国旗 】:
【荷兰国旗实质】:
当小于区域推着等于区域撞上了大于区域,整个问题结束了~~~!!!
【荷兰国旗思路】:
1)i < num , i 和 <区 下一个交换 , <区右扩, i++ ;
2)i=num , i++ ;
3)i>num , i和 >区前一个交换 , >区左扩 , i 原地不变。
注意:两元素交换不用第三方,这两元素不能是同一物理空间上的元素!!!
import java.util.Arrays;
public class Code2_荷兰国旗 {
public static int[] guoQiSort(int[] arr, int num) {
int p1 = 0;
int i = 0;
int p2 = arr.length - 1;
while (i <= p2) { //这个地方判定条件是DEBUG出来的 //因为有可能[i] 和 [p2]是挨着的~
if (arr[i] < num) {
int a = arr[i];
arr[i++] = arr[p1];
arr[p1++] = a;
} else if (arr[i] > num) {
int a = arr[i];
arr[i] = arr[p2];
arr[p2--] = a;
} else {
i++;
}
}
return arr;
}
public static void main(String[] args) {
int[] arr = {3, 5, 6, 3, 4, 5, 2, 9};
int[] b = guoQiSort(arr, 5);
Arrays.stream(b).forEach(value -> System.out.print(" " + value));
System.out.println();
int[] arr1 = {3, 5, 6, 3, 4, 5, 1, 99, 2, 9, 1, 11};
int[] b1 = guoQiSort(arr1, 5);
Arrays.stream(b1).forEach(value -> System.out.print(" " + value));
}
}
【快排v1v2】:
不管是v1还是v2 , 时间复杂度都是 O(N方) , 因为可以举出最差的例子:
//划分值打的很偏,产生了很差的情况(如下图,很偏左/很偏右的情况)~~~!!!
虽然快1快2没3厉害,但是了解快1快2,才能得知为何快3更优秀( 数学概率,长期期望 )。
【快排v2的实质】:
看视频的时候,加载条并不是从左到右顺序加载的,而是从中间一段一段加载的,我想这不就像是v2么,v2的高级之处在于:它一次至少能搞定一批数。
【快排v3】:
数学上的长期期望为——O( N logN )
import java.util.Arrays;
public class Code02_QuickSort_Version3 {
public static void quickSort( int[] arr ){
if ( arr==null || arr.length<2 ){
return;
}
quickSort(arr , 0 , arr.length-1);
}
// arr[L...R] 排好序
public static void quickSort( int[] arr , int L , int R ){
if ( L < R ){
swap( arr , L+(int)( Math.random()*(R-L+1) ) , R ); //第一步随机选一个位置,将其和最右侧的数做一下交换。
int[] p = partition(arr, L,R); //返回的这个数组是全部的等于区域的数组;
quickSort(arr, L, p[0]-1); // p[0]-1 是 小于区域的右边界; //《 区。
quickSort(arr, p[1]+1, R); // p[1]+1 数 大于区域的左边界; // 》区。
}
}
//这是一个处理 arr[L...R] 的函数
//默认以 arr[R] 做划分 , arr[R] -> p <p ==p >p
//返回等于区域(左边界,右边界),所以返回一个长度为2的数组 res , res[0] , res[1]
public static int[] partition( int[] arr , int L , int R ){
int less = L-1; //<区右边界
int more = R; //>区左边界
while ( L < more ){ //L表示当前数的位置 arr[R] -> 划分值
if ( arr[L] < arr[R] ){
swap( arr , ++less , L++ );
}else if ( arr[L] > arr[R] ){
swap( arr , --more , L );
}else {
L++;
}
}
swap(arr,more,R);
return new int[] {less+1 , more};
}
public static void swap(int[] arr , int i , int j ){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
int[] arr={ 3,6,2,111,5,7,5,99 };
quickSort(arr);
Arrays.stream(arr).forEach(value -> System.out.print(value+" "));
}
}