二叉树结构:
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(){}
TreeNode(int val){this.val=val;}
TreeNode(int val, TreeNode left, TreeNode right){
this.val=val;
this.left=left;
this.right=right;
}
}
以前序遍历为例:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> lint=new LinkedList<>();
preorder(root, lint);
return lint;
}
public void preorder(TreeNode root, List<Integer> lint){
if(root==null)return;
lint.add(root.val);//前序、中序、后序的区别是这一句的位置
preorder(root.left, lint);
preorder(root.right, lint);
}
以后序遍历为例(稍微特殊):
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> lint=new LinkedList<>();
Deque<TreeNode> myq = new LinkedList<>();
TreeNode node = root,prev=null;
while(!myq.isEmpty()||node!=null){
while(node!=null){//找到当前节点的最左的没有遍历过的节点
myq.push(node);
node=node.left;
}
node=myq.pop();
if(node.right==null||node.right==prev){//如果当前节点没有右节点或者,当前节点的右节点已将被遍历了
lint.add(node.val);
prev=node;
node=null;//**非常重要,当前节点值加入列表后,说明其左右节点已经遍历过了,要继续朝父节点遍历,所以要置空,不要再朝左右节点深入了
}
else{//当前节点有右节点,则将当前节点入栈,并将右节点变为当前节点
myq.push(node);
node=node.right;
}
}
return lint;
}
前序、中序的区别是中间节点加入列表的位置,但后序将会多出一个判断当前节点的右节点是否为空或者已经被遍历的情况,还有需要注意的是,当前节点加入列表意味着当前节点已经遍历结束了,需要置空,否则会重复遍历左子节点。
用队列Queue,先进先出,加元素是offer,删元素是poll
Queue<TreeNode> myq=new LinkedList<>();myq.offer(null);myq.poll();
每一层循环开始时,队列里是当前层的所有元素。
需要多使用一个栈存放每一个节点对应的深度。以判断是不是右视图中的点。同一深度下输出最右的节点。
//1深度优先搜索,非递归
//时间复杂度,O(n),深度优先搜索最多访问每个结点一次
//空间复杂度,O(n),最坏情况下,栈内会包含接近树高度的结点数量,占用 O(n) 的空间
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res=new LinkedList<>();
Map<Integer, Integer> right_first = new HashMap<>();
int max_depth=-1;
Deque<TreeNode> nodestack = new LinkedList<>();//记录节点
Deque<Integer> depthstack = new LinkedList<>();//记录每个节点对应的深度
//输入根节点以及根节点的深度
nodestack.push(root);
depthstack.push(0);
while(!nodestack.isEmpty()){
TreeNode node=nodestack.pop();
int depth=depthstack.pop();
if(node!=null){//防止空指针
if(max_depth<depth){
max_depth=depth;//记录最大深度
}
if(!right_first.containsKey(depth)){//如果不存在对应深度的节点才插入到hashmap
right_first.put(depth,node.val);
}
nodestack.push(node.left);//模拟栈,后进先出,使用push,pop
nodestack.push(node.right);
depthstack.push(depth+1);
depthstack.push(depth+1);
}
}
for(int i=0; i<=max_depth; i++){//因为把根节点当0层
res.add(right_first.get(i));
}
return res;
}
不需要多使用一个栈,但需要将结果list设为全局变量。
//2深度优先搜索,递归, 中序遍历一般是中左右,此时为了每一层最先访问最右的元素,采用中右左。
List<Integer> res=new LinkedList<>();
public List<Integer> rightSideView(TreeNode root) {
dfs(root, res, 0);
return res;
}
public void dfs(TreeNode root, List<Integer> res, int depth){
if(root==null)return;
if(depth==res.size())res.add(root.val);
dfs(root.right, res, depth+1);//为了最先访问每一层的最右的元素
dfs(root.left, res, depth+1);
}
遍历每一层,并输出每一层最右的那个节点。
使用队列,对于最外层循环,每一次循环时,队列里装的是当前层的所有元素,可以按中右左的方法存放,这样每一层第一个元素即右视图元素。
定义:二叉树的每个节点的左右子树的高度差的绝对值不超过 1,则二叉树是平衡二叉树
//时间复杂度是O(n2),最坏的情况:二叉树是满二叉树,需要遍历二叉树中的所有节点,时间复杂度是 O(n)。
//对于节点 p,如果它的高度是 d,则 height(p) 最多会被调用 d 次.
//最坏的情况,二叉树形成链式结构,高度为 O(n),此时总时间复杂度为 O(n2)
//一般情况下,二叉树的高度是logn,此时总时间复杂度为O(nlogn)
//空间复杂度是 O(n),空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
public boolean isBalanced(TreeNode root) {
if(root==null)return true;
//当根节点左右子树高度差不超过1,且左右子树都是平衡二叉树时,该树是平衡二叉树
else return Math.abs(height(root.left)-height(root.right))<=1&&isBalanced(root.left)&&isBalanced(root.right);
}
public int height(TreeNode root){//求节点的深度
if(root==null)return 0;
else return Math.max(height(root.left), height(root.right))+1;
}
自底向上递归的做法类似于后序遍历,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡.
//时间复杂度O(n):每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O(n)。
//空间复杂度O(n):空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
public boolean isBalanced(TreeNode root) {
return height(root)>=0;//当为不平衡二叉树的时候会返回-1
}
public int height(TreeNode root){//求节点的深度
if(root==null)return 0;
int leftheight=height(root.left);
int rightheight=height(root.right);
if(rightheight==-1||leftheight==-1||Math.abs(leftheight-rightheight)>1)return -1;//若当前节点不是平衡二叉树则返回-1
else return Math.max(leftheight, rightheight)+1;//否则返回树的高度
}
先翻转左右子树,再交换左右子树。
//时间复杂度:O(n),会遍历二叉树中的每一个节点,对每个节点而言,我们在常数时间内交换其两棵子树。
//空间复杂度:O(n),使用的空间由递归栈的深度决定,它等于当前节点在二叉树中的高度,最坏情况下,树形成链状,空间复杂度为 O(N)。
时间复杂度:O(n^2)
空间复杂度:O(1)
将已经遍历到的数据以及下标存入hash表,对于数组中的每个数字,判断hash表中是否有sum-当前数的存在,如果有就结束循环。
思路:分治思想
分:在数组a[0…n]中选择一个数a[q],其中0<=q<=n。将数组分为a[0…q-1]和a[q+1…n]两部分,其中a[0…q-1]中的数都小于a[q],a[q+1…n]中的数都大于a[q]。
治:由于在数组中,直接交换了数据的位置,所以不需要合并的过程,其他地方是需要的
将交换数组两个元素的代码和将一个数组分开的代码抽离成swap函数和partition函数。
//快排
Random random = new Random();
public void quicksort(int[] nums, int s, int e){
if(s>=e)return;
int q=random.nextInt(e-s+1)+s;//随机一个[s...e]之间的数
swap(nums, q, e);//将随机的nums[q]换到这一段数组的最后一个位置
int qindex=partition(nums, s, e);//获取分好后的q的下标
quicksort(nums, s, qindex-1);
quicksort(nums, qindex+1, e);
}
//交换数组中两个数的位置
public void swap(int[] nums, int a, int b){
int t=nums[a];
nums[a]=nums[b];
nums[b]=t;
}
//负责将数组分为大于nums[q]和小于等于nums[q]两部分,其中nums[q]存放在该段数组最后一个位置
//返回划分后的nums[q]的下标
public int partition(int[] nums, int s, int e){
int x=nums[e];//记录对比对象nums[q]
int low_index=s;//记录比nums[q]小的数组交换的下标
for(int i=s; i<e; i++){
if(nums[i]<=x)swap(nums, low_index++, i);
}
swap(nums,low_index, e);
return low_index;
}
由于此题并不是要排序,而是为了要把第k大的元素输出,那么若排好序,第k大的元素必定在数组的a[a.length-1-k]的位置上。所以只需要递归对含有第k大个元素的那部分进行快排,而不需要走完全程。
不一样地方:
public int findKthLargest(int[] nums, int k) {
return quicksort(nums, 0, nums.length-1, nums.length-k);
}
//快排
public int quicksort(int[] nums, int s, int e, int index){//int类型都是数组下标
int q=randomPartition(nums, s, e);
if(q==index)return nums[q];
//选择包含第K大元素的部分递归进行快排
return q<index?quicksort(nums, q+1, e, index):quicksort(nums, s, q-1, index);
}
//选择随机的q,并分开数组
public int randomPartition(int[] nums, int s, int e){
int q=random.nextInt(e-s+1)+s;//随机一个[s...e]之间的数
swap(nums, q, e);//将随机的nums[q]换到这一段数组的最后一个位置
return partition(nums, s, e);
}
堆:堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。
大顶堆:每个结点的值都大于或等于其左右孩子结点的值。
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
完全二叉树:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。
满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
思想:对于一颗完全二叉树,从最底层的非叶子节点开始,将该子树转换成大顶堆(或小顶堆)。注意:对于高层非叶子节点,调整完当前节点后记得递归将发生改变的子节点(非叶子节点)进行调整。
//建大顶堆
public void buildMaxHeap(int[] nums, int heapSize){
//对所有非叶子节点进行大顶堆的调整,从最低的非叶子节点开始。
for(int i=heapSize/2; i>=0; i--){
maxHeapify(nums, i, heapSize);
}
}
//交换数组中的两个数
public void swap(int[] nums, int a, int b){
int t=nums[a];
nums[a]=nums[b];
nums[b]=t;
}
//将当前节点大顶堆化
public void maxHeapify(int[] nums, int curr, int heapSize){
//对于顺序存储的完全二叉树,非叶子节点curr的子节点若存在,则必定是2*curr+1和2*curr+2
int l=curr*2+1, r=curr*2+2, max=curr;
if(l<heapSize&&nums[l]>nums[max])max=l;
if(r<heapSize&&nums[r]>nums[max])max=r;//判断三个点哪个点的数字最大,将当前节点与最大的那个交换
if(max!=curr){
swap(nums, max, curr);
maxHeapify(nums, max, heapSize);//**重要!交换之后再进行下一层的大顶堆调整
}
}
对于找第k大的数,首先要讲数组构建成完全二叉树,然后将树调整为大顶堆,之后再删除k-1次堆顶元素,每次删除后,都要将当前堆的最后一个元素放在堆顶,然后将树重新调整为大顶堆。
public int findKthLargest(int[] nums, int k) {
int headSize=nums.length;
buildMaxHeap(nums, heapSize);//第一次建大顶堆,堆中元素还没有删除
for(int i=nums.length-1; i>=nums.length-k+1; i--){
swap(nums, 0, i);//此时已经是大顶堆,此行代码执行删除并调整最后的元素到堆顶的操作
heapSize--;
maxHeapify(nums, 0, heapSize);//重新调整为大顶堆
}
}
思想:每冒泡循环一次将最大的移动到最后,注意内循环的长度。
public void BubbleSort(int[] nums){
for(int i=0; i<nums.length; i++){
for(int j=0; j<nums.length-1-i; j++){
//对于每次内循环,作用都是将当前无序的部分的最大值放在无序部分的最后一个位置,i=0的时候,j最大是nums.length-2
//i=0时,循环结束将把最大的数移动到nums.length-1的位置
if(nums[j]>nums[j+1]){
//交换位置
int temp=nums[j];
nums[j+1]=nums[j];
nums[j+1]=temp;
}
}
}
}
思想:由于单个数是有序的,所以从第二个数开始,寻找每个数应该插入到有序的部分的位置,从有序部分的最后部分开始对比,边比边移动位置。
public void insertSort(int[] nums){
int j=0;//记录插入的位置
for(int i=1; i<nums.length; i++){
int curr=nums[i];
for(j=i-1; j>=0; j--){
if(nums[j]>curr)nums[j+1]=nums[j];//后移寻找合适位置
else break;
}
nums[j+1]=curr;//将当前数插入,因为循环过后j--了,所以要插入j+1的位置上
}
}
思想:自顶向下,将数组分为两份,分别进行归并排序,再将两部分合起来;自底向上,每两个两个比较,再四个四个比较。。直到n个
// 归并排序(Java-迭代版)
public static void merge_sort(int[] arr) {
int len = arr.length;
int[] result = new int[len];
int block, start;
// 原版代码的迭代次数少了一次,没有考虑到奇数列数组的情况
for(block = 1; block < len*2; block *= 2) {
for(start = 0; start <len; start += 2 * block) {
int low = start;
int mid = (start + block) < len ? (start + block) : len;
int high = (start + 2 * block) < len ? (start + 2 * block) : len;
//两个块的起始下标及结束下标
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
//开始对两个block进行归并排序
while (start1 < end1 && start2 < end2) {
result[low++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
}
while(start1 < end1) {
result[low++] = arr[start1++];
}
while(start2 < end2) {
result[low++] = arr[start2++];
}
}
int[] temp = arr;
arr = result;
result = temp;
}
result = arr;
}
// 归并排序(Java-递归版)
static void merge_sort_recursive(int[] arr, int[] result, int start, int end) {
if (start >= end)
return;
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, result, start1, end1);
merge_sort_recursive(arr, result, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2)
result[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
result[k++] = arr[start1++];
while (start2 <= end2)
result[k++] = arr[start2++];
for (k = start; k <= end; k++)
arr[k] = result[k];
}
public static void merge_sort(int[] arr) {
int len = arr.length;
int[] result = new int[len];
merge_sort_recursive(arr, result, 0, len - 1);
}
思想:桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。
思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
思想:希尔排序的实质就是分组插入排序,该方法又称递减增量排序算法,因DL.Shell于1959年提出而得名。希尔排序是非稳定的排序算法。先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。 设 gap=N/2= 5,即相隔距离为 5 的元素组成一组,以此类推。
思想:计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
① 分配。扫描一遍原始数组,以当前值-minValue作为下标,将该下标的计数器增1。
② 收集。扫描一遍计数器数组,按顺序把值收集起来。
题目:自然数数组的排序:给定一个长度为N的整形数组arr,其中有N个互不相等的自然数1-N,请实现arr的排序,但是不要把下标0N-1位置上的数通过直接赋值的方式替换成1N,要求时间复杂度为O(n),空间复杂度为O(1)。
public static void sort(int n, int[] nums){
// int count=0;
// for(int i=0; i
// if(i!=nums[i]-1){
// count ++;
// swap(nums, i, nums[i]-1);
// i--;
// }
// }
//将数字换到它该在位置上去
for(int i=0; i!=nums.length; i++){
while(i!=nums[i]-1)swap(nums, i, nums[i]-1);
}
}
public static void swap(int[] nums, int i, int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
注意:时间复杂度看的是交换的次数。
双指针是一种思想,一种技巧或一种方法,并不是什么特别具体的算法,在二分查找等算法中经常用到这个技巧。具体就是用两个变量动态存储两个或多个结点,来方便我们进行一些操作。通常用在线性的数据结构中,比如链表和数组,有时候也会用在图算法中。
类似于龟兔赛跑,两个链表上的指针从同一节点出发,其中一个指针前进速度是另一个指针的两倍。
解决问题的类型:
fastptr和slowptr都从头节点出发,每轮迭代中 ,fastptr向前移动两个节点,slowptr向前移动一个节点,最终当快指针到达终点的时候,慢指针刚好在中间的节点。
如果链表中有环,fastptr和lastptr会在环中相遇。
当判断出链表中有环时,在相遇时,将其中一个指针重新指向头节点,然后两个指针以相同速度前进,再次相遇时的位置就是环的起点。
两个指针相遇后,只要一个不动,另外一个继续走直到相遇。
先让fastptr提前走k步,然后slowptr从头开始和fastptr一起走,当fastptr走到链表尾,则slowptr指向的就是第k个元素。
一般都是排好序的数组或链表。
解决问题的类型 :
框架:
public int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = (right + left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...//重要
} else if (nums[mid] > target) {
right = ...//重要
}
}
return ...;
}
特殊情况:
①寻找target的最左侧边界问题,即找到数组中最左的那个目标值:数组是有序的,递增关系。
public int left_bound(int[] nums, int target){
int left=0, right=nums.length;//注意此处区间是[left,right)
while(left<right){
int mid=(left+right)/2;
if(nums[mid]==target)right=mid;//为了找到最左的target的下标
else if(nums[mid]<target)left=mid+1;//区间转到[mid+1,right)
else if(nums[mid]>target)right=mid;//区间转到[left, mid)
}//循环结束时,left=right
if(left==nums.length)return -1;//此时数组中不存在该目标
if(nums[left]!=target)return -1;
return left;
}
②寻找target的最右侧边界问题,即找到数组中最右的那个目标值:数组是有序的,递增关系。
public int right_bound(int[] nums, int target){
int left=0, right=nums.length;
while(left<right){
int mid = (left+right)/2;
if(nums[mid]==target)left=mid+1;
else if(nums[mid]<target)left=mid+1;
else if(nums[mid]>target)right=mid;
}//结束的时候left=right
if(left==0)return -1;
if(nums[right-1]!=target)return -1;
return right-1;
//循环结束的时候left=right,当执行到这里的时候,left必定是不等于0,那么left必定经过一次left=mid+1的赋值,
//所以mid=left-1
//而上述已经判断过了nums[left-1]是否等于
}
比如两数之和问题,先对数组排序然后左右指针找到满足条件的两个数。如果是三数问题就转化为一个数和另外两个数的两数问题。以此类推。
例题:LeetCode_P1_TwoSum、LeetCode_P15_ThreeSum、LeetCode_P18_FourSum。
两个指针,一前一后组成滑动窗口,并计算滑动窗口中的元素的问题。
//双向链表+hash表
class LRUCache {
//维护一个双向链表
private class BiLinkNode {
int key;
int val;
BiLinkNode prev;
BiLinkNode next;
BiLinkNode(){}
BiLinkNode(int key, int val) {
this.key=key;
this.val=val;
}
}
private BiLinkNode head;
private BiLinkNode tail;
Map<Integer, BiLinkNode> cache=new HashMap<>();//缓存
private int size;//当前缓存大小
private int capa;//缓存最大容量
//删除双向链表中的一个节点
private void removeNode(BiLinkNode node) {
}
//删除双向链表中最后一个节点并返回该节点
private BiLinkNode deleteTail() {
}
//将一个新的节点添加至链表的表头
private void addToHead(BiLinkNode node) {
}
//讲一个链表中某一个节点移动至表头
private void moveToHead(BiLinkNode node) {
}
//初始化一个缓存,不仅要初始化花村容量,还要初始化节点个数为0,同时初始化双向链表
public LRUCache(int capacity) {
}
//get操作,确认缓存中是否存在该,不存在则返回-1,存在则将该节点移至表头并范围节点的value
public int get(int key) {
}
//put操作,先判断缓存中是否已经存在该键值对,如果存在,则先更新value再移至表头;
//如果不存在,则先将节点加入表头和hash表,然后判断是否溢出,若溢出则删除链表尾部的节点,并从hash表中移除
public void put(int key, int value) {
}
}
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner s=new Scanner(System.in);
//读入int
int n=s.nextInt();
//读入String
String str=s.next();
//读入下一行
String line=s.nextLine();
}
}