DAY1
DAY2(认识复杂度和简单排序算法)
时间复杂度:常数时间指的是和数据量无关的操作所用的时间。在O(f(n))中只要高阶项。 评价算法流程的指标。
先看时间复杂度指标,如果相同,进行实际操作进行比较。
选择排序
public static void selectionSort(int[] arr){ if(arr==null||arr.length<2){ return; } for(int i=0;i<arr.length-1;i++){ int minIndex=i; for(int j=i+1;j<arr.length;j++){ minIndex=arr[j]<arr[minIndex]?j:minIndex; } swap(arr,i,minIndex); } } public static void swap(int[] arr,int i,int j){ int temp=arr[i]; arr[i]=arr[j]; arr[j]=temp; }
冒泡排序
public static void BubbleSort(int[] arr){ if(arr==null||arr.length<2){ return; } for(int e=arr.length-1;e>0;e--){ for(int i=0;i<e;i++){ if(arr[i]>arr[i+1]){ swap(arr,i,i+1); } } } }
位运算交换两数(原理和性质相关)-值可以相等,内存需要是两块东西
//交换arr数组中,i和j的位置 public static void swap(int[] arr,int i,int j){ arr[i]=arr[i]^arr[j]; arr[j]=arr[i]^arr[j]; arr[i]=arr[i]^arr[j]; } //抖机灵写法,不用开辟新的内存空间
tips:异或^运算可以理解成无进位相加。(二进制中)
0 ^ N = N , N ^ N = 0
异或运算满足交换律和结合律.
提取一个数最右边的1
int rightOne=eor&(~eor+1);
插入排序
//本质是小范围有序,加入一个数,比较后插入对应位置 public static void insertionSort(int[] arr) { if (arr == null || arr.length < 2) { return; } for (int i = 1; i < arr.length; i++) { for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) { swap(arr,j,j+1); } } }
二分法查找
(不熟练,自己学)
循环不变量和左闭右闭,左开右开的写法。
left right 和mid的大小关系。
由mid下标所在取值来看是否和while中定义的条件冲突,需要去符合条件。
对数器
想测的方法A---->好实现,好想通的方法B
生成一个随机样本产生器
大数据不一样----》缩小数据,人工干预
调整对了以后再用大量数据测试--》成功即正确
DAY3(认识O(NlogN)的排序)
三种不同顺序的遍历:
DLR--前序遍历(根在前,从左往右,一棵树的根永远在左子树前面,左子树又永远在右子树前面 ) LDR--中序遍历(根在中,从左往右,一棵树的左子树永远在根前面,根永远在右子树前面) LRD--后序遍历(根在后,从左往右,一棵树的左子树永远在右子树前面,右子树永远在根前面)
//arr[L,R]范围上求最大值【递归加分治,查找最大值二分,L=R是终止条件】 public static int process1(int[] arr,int L,int R){ if(L==R){ return arr[L]; } int mid=L+((R-L)>>1); int maxLeft=process1(arr,L,mid); int maxRight=process1(arr,mid+1,R); return Math.max(maxLeft,maxRight); }
master公式:
T(N)=a * T(N/b)+O(N的d方)
母 = 调用次数*子+剩下过程的时间复杂度
归并排序
//归并排序--->先左右各部分排序,再归并MergeSort //外排序的方法-》新开辟了一块空间 //master公式求时间复杂度 //时间复杂度 O(N*logN) 额外空间复杂度O(N) public static void process2(int[] arr,int L,int R){ if(L==R){ return; } int mid=L+((R-L)>>1); process2(arr,L,mid); process2(arr,mid+1,R); merge(arr,L,mid,R); } public static void 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; while(p1<=M&&p2<=R){ 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(i=0;i<help.length;i++){ arr[L+i]=help[i]; } }
相较于归并排序,前几种排序浪费了大量的比较行为。
拓展:小和问题(相等情况先拷贝右边),逆序对问题(改写罢了,原理一样)
//小和问题(改写归并排序) public static int process3(int[] arr,int l,int r){ if(l==r){ return 0; } int mid=l+((l-r)>>1); return process3(arr,1,mid)+process3(arr,mid+1,r)+merge2(arr,l,mid,r); } public static int merge2(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; 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(i=0;i<help.length;i++){ arr[L+i]=help[i]; } return res; }
快速排序(最坏O(N2))
随机快排3.0版本O(NlgoN)
(荷兰国旗问题)
快排有点像二分
运算规划取决于划分值的取值
//快速排序 //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);//3.0版本,随机选一个数 int[]p=partition(arr,L,R); quickSort(arr,L,p[0]-1); quickSort(arr,p[1]+1,R); } } //这是一个处理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){ arr[i]=arr[i]^arr[j]; arr[j]=arr[j]^arr[i]; arr[i]=arr[i]^arr[j]; }
DAY4(详解桶排序及排序内容总结)
tips:快速排序空间复杂度最大使O(N),开了N层划分.最小是O(logN)
堆排序
完全二叉树/满二叉树
数组从0出发的连续的一段可看作一颗完全二叉树
大根堆:以某个结点为头的整棵树,最大值是这个结点的值
小根堆:同理改成小。
如何构建大根堆->根据i位置和i的父位置i-1/2比较,循环比较交换,终止条件是i位置小于父位置i-1/2的值.(heapinsert过程 )
heapinsert:
//大根堆构建 public static void heapInsert(int[] arr,int index){ while(arr[index]>arr[(index-1)/2]){ swap(arr,index,(index-1)/2); index=(index-1)/2; } }
heapify
//取出大根堆的最大值,并不改变大根堆的特性.heapify堆化 public static void heapify(int[] arr,int index,int heapSize){ int left=index*2+1;//左孩子的下标 while(left<heapSize){//下方还有孩子,因为左孩子下标肯定小于右孩子下摆,所以判断左孩子即判断有没有孩子 //两个孩子中,谁的值大就把下标给largest int largest=left+1<heapSize&&arr[left+1]>arr[left]?left+1:left; //父和孩子之间,谁的值大,谁把下标给largest largest=arr[largest]>arr[index]?largest:index; if(largest==index){ break; } swap(arr,largest,index); index=largest; left=index*2+1; } }
堆最基础的两个方法。
堆排序
O(NlogN)
//堆排序的coding public static void heapSort(int[] arr){ if(arr==null&&arr.length<2){ return; } for(int i=0;i<arr.length;i++){ heapInsert(arr,i); } int heapSize=arr.length; swap(arr,0,--heapSize); while(heapSize>0){ heapify(arr,0,heapSize); swap(arr,0,--heapSize); } }
在时间复杂度O(NlogN)的级别上只有堆排序能做到空间复杂的O(1)
时间复杂度和完全二叉树高度有关
拿到数组可以不用从第一个开始插入heapInsert,可以从最后开始heapify(这一步时间复杂度为O(N)),只有第一部变快了一点。
可修改代码为:
//堆排序的coding public static void heapSort(int[] arr){ if(arr==null&&arr.length<2){ return; } /*for(int i=0;i<arr.length;i++){ heapInsert(arr,i); }*/ for(int i=arr.length-1;i>=0;i--){ heapify(arr,i,arr.length); } int heapSize=arr.length; swap(arr,0,--heapSize); while(heapSize>0){ heapify(arr,0,heapSize); swap(arr,0,--heapSize); } }
小根堆,java中:
//优先队列,本质就是小根堆 PriorityQueue<Integer> heap=new PriorityQueue<Integer>(); //如果要大根堆,传入比较器 PriorityQueue<Integer> heap=new PriorityQueue<Integer>(new AComp()); //对应方法自己查
手写堆和用java提供的数据结构堆的区别就是,java给的细粒化程度低,相当于黑盒,手写可以自由操控。
手写堆有的题目必须用,很重要!!
比较器的使用
(不熟悉,要加强练习,比较器几乎没用过)
pubic static class IdAscendingCompartor implements Comparator<Student>{ //返回负数的时候,第一个参数排在前面 //返回正数的时候,第二个参数排在前面 //返回0的时候,谁在前面无所谓 @Override public int compare(Student o1,Student o2){ //理解不了就从 if(o1>o2) return 1;开始推 return o1.id-o2.id; //return o2.id-o1.id; } } //系统排序(在main方法里) Arrays.sort(students,new IdAscendingComparator());
计数排序
词频统计
基数排序(桶排序,桶就是容器,可以用不同的数据结构)--有点难理解,但讲解过后也还好。
不基于比较的排序,一定要看数据的状况,进行定制
代码:
//桶排序(基数排序) public static void radixSort(int[] arr,int L,int R,int digit){ final int radix=10; int i=0,j=0; //有多少数准备多少个辅助空间 int[] bucket=new int[R-L+1]; //有多少位就进出几次 for(int d=1;d<=digit;d++){ //10个空间 //count[0]当前位(d位)是0的数字有多少个 //count[1]当前位(d位)是(0和1)的数字有多少个 //count[2]当前位(d位)是(0,1,2)的数字有多少个 //count[i]当前位(d位)是(0~i)的数字有多少个 int[] count=new int[radix]; for(i=L;i<=R;i++){ // j=getDigit(arr[i],d); count[j]++; } for(i=1;i<radix;i++){ count[i]=count[i]+count[i-1]; } for(i=R;i>=L;i--){ // j=getDigit(arr[i],d); bucket[count[j]-1]=arr[i]; count[j]--; } for(i=L,j=0;i<=R;i++,j++){ arr[i]=bucket[j]; } } }
精妙的算法!可惜我不会......
DAY5
看牙去了,摸鱼一天。。。。。
希尔排序-》多轮插入排序
排序算法多的稳定性:
排序前后相同数的相对次序。
非基础类型排序非常有用。
选择排序不能,冒泡排序可以, 插入排序可以,归并可以(NlogN),主要注意处理相等时候的做法
快排做不到稳定性,堆排序做不到稳定性
排序总结:
工程上改进,例如小样本量用插入排序,大样本量用快排
DAY6(链表)
哈希表
无序组织
map和key本质一样,map是key value对应的。
Hashmap的增删改查都是常数级别时间复杂度O(1)。
按放入哈希表的数据类型可分为:直接传值(基础类型),传引用(地址8位).
有序表
(TreeMap/TreeSet)-->java中
有序组织!,性能是O(logN)级别。
按放入有序表的数据类型可分为:直接传值,值大小比较排序(基础类型),比较器比较(引用地址传递).
链表
Coding:反转单向链表和反转双向链表自己练(注意返回值)
Coding:打印两个有序链表的公共部分。
tips:额外数据结构记录(哈希表),快慢指针
Coding:回文链表。
Coding:复制带随机指针的链表/第二种方法
+++二叉树今天第二节课
就听了个题目
DAY7
返回两个不知是否有环链表若相交的第一个入环节点:
链表是否有环
有环:
返回环的入结点(借助HashSet实现,不重复特点)
不借助Hashset,快2慢1指针,若有环一定会相遇。-》之后快指针到头结点,开始快慢各一次走1,一定在入环结点相遇。
Coding:
//找到链表第一个入环节点,如果无环,返回null public static Node getLoopNode(Node head){ if(head==null||head.next==null||head.next.next==null){ return null; } //初始条件是不能相等的 Node n1=head.next; Node n2=head.next.next; while(n1!=null){ if(n2.next==null||n2.next.next==null){ return null; } n2=n2.next.next; n1=n1.next; } //快指针移动到头部 n2=head; while(n1!=n2){ n1=n1.next; n2=n2.next; } return n1; }
两个链表都无环,即=null:
各自遍历到最后结点,分别统计长度,判断两链表最后一个结点是否相同.
相同则重新回到起点,长的链表先走(长-短步)
Coding:
//如果两个链表都无环,找到第一个相交节点,如果不相交,返回null public static Node noLoop(Node head1,Node head2){ if(head1==null||head2==null){ return null; } int n=0; Node cur1=head1; Node cur2=head2; while(cur1!=null){ n++; cur1=cur1.next; } while(cur2!=null){ n--; cur2=cur2.next; } if(cur1!=cur2){ return null; } cur1=n>0?head1:head2;//cur1变成长链表的头 cur2=cur1==head1?head2:head1;//cur2变成短链表的头 n=Math.abs(n); while(n!=0){ n--; cur1=cur1.next; } while(cur1!=cur2){ cur1=cur1.next; cur2=cur2.next; } return cur1; }
两个链表都有环,但是不相交:
共用环,但是入环结点相同;
共用环,入环结点不同:(这类题思路很重要,代码可以复用 )
//两个有环链表,返回第一个相交节点,如果不相交返回null public static Node bothLoop(Node head1,Node loop1,Node head2,Node loop2){ Node cur1=null; Node cur2=null; if(loop1==loop2){ cur1=head1; cur2=head2; int n=0; while(cur1!=loop1){ n++; cur1=cur1.next; } while(cur2!=loop2){ n--; cur2=cur2.next; } cur1=n>0?head1:head2; cur2=cur1==head1?head2:head1; n=Math.abs(n); while(n!=0){ n--; cur1=cur1.next; } return cur1; }else { cur1=loop1.next; while(cur1!=loop1){ if(cur1==loop2){ return loop1; } cur1= cur1.next; } } return null; }
DAY8(二叉树继续)
7/26!!MC玩腻了,糖糖复活!!!
递归方法遍历二叉树
递归序中每个结点经过三次。
先序遍历(简化递归序,第一次来到结点打印,头左右的顺序)
中序遍历(简化递归序,第二次来到结点打印,左头右的顺序)
后序遍历(简化递归序,第三次来到结点打印,左右头的顺序)
将递归代码中按递归序分成三个区域:
public static void preOrderRecur(TreeNode head){ if(head==null){ return; } //第一次经过结点 System.out.println(head.val); preOrderRecur(head.left); //第二次经过结点 preOrderRecur(head.right); //第三次经过结点 }
非递归遍历二叉树
借助栈来实现:
先序遍历
//非递归先序遍历 public static void preOrderUnRecur(TreeNode head){ if(head==null){ return; } Stack<TreeNode> stack=new Stack<TreeNode>(); stack.push(head); while(!stack.isEmpty()){ head=stack.pop(); System.out.println(head.val); if(head.right!=null){ stack.push(head.right); } if(head.left!=null){ stack.push(head.left); } } }
后序遍历
借助另一个收集栈,先左后右,把打印步骤换成入栈,最后打印收集栈,即得入栈的逆序,即左右头,后序遍历实现.
public static void posOrderUnRecur(TreeNode head){ if(head==null){ return; } Stack<TreeNode> stack1=new Stack<>(); Stack<TreeNode> stack2=new Stack<>(); stack1.push(head); while(stack1!=null){ head=stack1.pop(); stack2.push(head); if(head.left!=null){ stack1.push(head.left); } if(head.right!=null){ stack1.push(head.right); } while(stack2!=null){ System.out.print(stack2.pop()+" "); } } }
中序遍历
注意左边界的概念:
public static void inOrderUnRecur(TreeNode head){ if(head==null){ return; } Stack<TreeNode> stack=new Stack<>(); while(!stack.isEmpty()||head!=null){ if(head!=null){ stack.push(head); head=head.left; }else { head=stack.pop(); System.out.println(head); head=head.right; } } }
能这么写的原因是树能够被左边界完全分解。
这几种遍历方法能够自己写出来!!!!
直观打印一颗二叉树
如何完成二叉树的宽度优先遍历(求一颗二叉树的宽度)
宽度优先遍历就是横着遍历:
//借助队列实现 public static void Width(Node head){ if(head==null){ return; } Queue<Node> queue=new LinkedList<>(); //JAVA中的队列就是LinkedList while(!queue.isEmpty()){ Node cur=queue.poll(); sout if(cur.left!=null){ queue.add(head.left); } if(cur.right!=null){ queue.add(head.right); } } }
求最大宽度:(借助哈希表实现,每个结点记录对应的层数,遍历时如果层数相同,则当前宽度+1,若不同则更新|每次让下个入栈的结点入表,记录对应的层数为当前层数+1)
代码:
//求二叉树的最大宽度 public static int BinaryWidth(TreeNode head){ if(head==null){ return 0; } Queue<TreeNode> queue=new LinkedList<>(); queue.add(head); HashMap<TreeNode,Integer> levelMap=new HashMap<>(); levelMap.put(head,1); int curLevel=1; int curLevelNodes=0; int max=Integer.MIN_VALUE; while(!queue.isEmpty()){ head=queue.poll(); int curNodeLevel=levelMap.get(head); if(curNodeLevel==curLevel){ curLevelNodes++; }else{ max=Math.max(curLevelNodes,max); curLevelNodes=1; } if(head.left!=null){ levelMap.put(head.left,curLevel+1); queue.add(head.left); } if(head.right!=null){ levelMap.put(head.right,curLevel+1); queue.add(head.right); } } max=Math.max(max,curLevelNodes); return max; }
DAY9
2023/8/06
如何判断一棵树是搜索二叉树?
搜索二叉树:一颗树里面,每一颗子树,左树都比节点小,右树都比节点大。
如何判断一棵树是完全二叉树?
按照宽度遍历:
1.任意结点,有右无左,返回false。
2.出现一个结点有左无右(不双全),后面所以结点都是叶节点(无左右孩子)
如何判断一棵树是不是满二叉树?
最大深度l,节点数N。
N=2(l次方) -1
如何判断一棵树是否是平衡二叉树?(二叉树题目套路)
停止~重心转移到另外的算法课上