算法刷题笔记总结记录

算法学习笔记总结记录

所有的算法题第一要义就是要明白题目讲什么,明白规律是什么,只有知道了规律才能写代码,才能轮到使用什么方法,什么工具,是用递归?还是循环?进而才能用到你总结的各种经验。

下面的题目除了动态规划,其他主要来自剑指offer的题目,而且主要是数据结构题 ,排序算法我们单独整理。

一、方法技巧总结:

1、递归模板以及使用技巧总结:

  • 一般递归如果如果有if --- else ,如果你没有设置全局变量的话,一般是需要两个return的,分别在不同情况作了什么,返回各自这种情况下的结果,如果有全局变量,那么一般是没有返回值的,可以见Code015_1(com.attackOnOffer.second.Code015_01)

    	public static void digui(ListNode listNode) {
    		if(listNode != null) {
    			digui(listNode.next);
    			res.next = new ListNode(listNode.val);
    			res = res.next;
            }
        }
    
  • 如果有两个数组一般就不用上下边界

  • 我们用到了返回boolean类型,一般样式是,xxx情况返回true,xxx情况返回fasle,最后返回xx && yy 这种样式。并且一定要有个返回true,一个要返回false;见017题

    public boolean method(){
    	if(情况1){
        	return true;
        }
        if(情况2){
            return false;
        }
        return method(x) & method(y);
    }
    
    
  • root.left = , root.right = 这种赋值操作,最后返回一个root ,参考004题

    root.left = digui();
    root.right = digui();
    
    return root; 
    
  • 当你遇到有界限的时候,果断把边界放到递归参数中,而前后边界的关系就是递归的出口

    public void method(int mid, int high, int low,int[] a){
        int start = low;
        int end = high;
        int i = 0;
        int hh = mid + 1;
        while(i <= start && hh <= high){ //有mid的时候
            // 这是归并的套路
        }
        while(low < high){ //不需要mid的时候
            // 这是快速的套路
        }
        if(low < high){
    		// 这是基本套路,退出递归
        }
    }
    
  • 通过返回一个特殊的东西,然后添加一个if判断,如果是特殊的,就继续返回特殊的,这样能提前终止递归.参考039题。

    public int method(int[] a, int count){
        if(count == -1){  // 通过过特殊判断退出递归
            return -1;
        }
        if(){
            count == -1;
            return method(a,count); // 某种情况返回-1
        }
    }
    
  • TreeNode node = KthNode(pRoot.left,k);不要迟疑直接先写,然后下面的 if(node != null){return node;} 记住这个模式 最后返回return null,记住这个模式!

  • 返回二叉树的模板

    public TreeNode digui(){
    	if(){                    
            return null;
        }
        if(){
            return new TreeNode(x);
        }
        root.left = digui();
        root.right = digui();
        return root;
    }    
    
  • 返回int的模板

    if(){
    	return 0;
    }
    int left = digui() + 1;
    int right = digui() + 1;
    return Math.max(left,right);  // 比较结果返回
    

2、思路总结:递归我们知道本质是重复做同一件事,经常的我们能够想明白这个事是怎么样的,但是却难以下手,为什么?因为如果从最开始的情况去思考,会很难理解,比如归并排序,一直二分下去,到最后只有一个元素,这怎么对比?怎么体现在代码上?容易思考得走火入魔。这个时候,我们可以直接把他当作中间的时候来写代码,这样十分容易。比如015循环算法,和归并排序(特别是归并)就是个例子。

3、说一下时间复杂度:1 < O(logn) <O(n) <O(nlogn) <O(n^2) <O(n^k) <O(2^n)

4、斐波那契数列使用的递归的复杂度是O(2^n),因为它可以看作一个颗二叉树,二叉树的高度是 n - 1,由我们的基础知识可以知道,一个高度为k的二叉树最多可以由 2^k - 1个叶子节点,也就是递归过程函数调用的次数,所以时间复杂度为 O(2^n),而空间复杂度就是树的高度 S(n)
在这里插入图片描述

二、链表题

链表题主要方法就是两个指针和递归和循环,我们先从这三个角度去思考。

  • 003-从尾到头打印链表

    办法:递归。不多提醒,你可以直接做出来。时间复杂度O(n)

    public static void method(ListNode p) {
    		if(p == null) {
    			return ;
    		}
    		method(p.next);
    		System.out.println(p.val);
    	}
    
  • 014-链表中倒数第k个结点

    经典的两个指针,一个先动,另外一个等待k。当然java没有指针,他是new两个对象等于原链表,时间复杂度O(n)

    	public static ListNode method(ListNode p,int k) {
    		ListNode p1 = p;
    		ListNode p2 = p;
    		int i = 0;
    		while(p1 != null && i <= k) {
    			p1 = p1.next;
    			i++;
    		}
    		while(p1 != null && p2 != null) {
    			p1 = p1.next;
    			p2 = p2.next;
    		}
    		return p2.next;
    	}
    
  • 015-反转链表

    三个办法,递归,利用栈,循环(这个稍微比较难写),但是复杂度都是O(n)

    // 递归办法1	-- 我给出递归主要是因为我发现我不是很习惯递归返回一个值,下面这个递归是没有返回值的
    	// 我貌似比较喜欢全局变量,也没有错
    	public static ListNode res = new ListNode(0);
    	
    	public static ListNode ReverseList(ListNode listNode) {
    		ListNode a = res;
    		digui(listNode);
    		return a.next;
        }
    	
    	public static void digui(ListNode listNode) {
    		if(listNode != null) {
    			digui(listNode.next);
    			res.next = new ListNode(listNode.val);
    			res = res.next;
            }
        }
    
    	// 递归办法2 --有返回值的
    	public static ListNode res;
    	
    	public static ListNode ReverseList(ListNode listNode) {
    		ListNode a = digui(listNode);
    		return res.next;
        }
    	
    	public static ListNode digui(ListNode listNode) {
    		if(listNode != null) {
    			ListNode res = digui(listNode.next);  // 这一步,你似乎不习惯递归返回值
    			res.next = new ListNode(listNode.val);
    			return res.next; // 因为一定要把这个东西返回,不然没有意义
            }else {
            	ListNode temp = new ListNode(0); // 因为只会进入这里一次
            	res = temp;
            	return temp;
            }
        }
    

    循环算法,我放一张图:

    这个就是中间状态,pre,next,head已经就位。但是正如我文章开头总结的一样,如果考虑最开始的pre=null思考怎么写代码,实在难以理解,无法下手。可以直接从下图的状态开始写,这个时候,我们要做的就是让head指向pre,pre移动到head的位置,next和head都移动到3的位置上。

在这里插入图片描述

public static ListNode method(ListNode p) {
	ListNode pre = null;
	ListNode head = p;
	ListNode next = null;
	while(head != null) {
		next = head.next;
		head.next = pre;
		pre = head;
		head = next;
	}
	return pre;
}
  • 016-合并两个或k个有序链表

    这个你要反应过来其实就是归并排序,当然了不需要递归,O(n)

    	public static ListNode method(ListNode p1,ListNode p2) {
    		ListNode res = new ListNode(0);
    		ListNode a = res;
    		while(p1 !=null && p2 != null) {
    			if(p1.val >= p2.val){
    				res.next = new ListNode(p2.val);
    				res = res.next;
    				p2 = p2.next;
    			} else {
    				res.next = new ListNode(p1.val);
    				res = res.next;
    				p1 = p1.next;
    			}
    		}
    		while(p1 !=null) {
    			res.next = new ListNode(p1.val);
    			res = res.next;
    			p1 = p1.next;
    		}
    		while(p2 !=null) {
    			res.next = new ListNode(p2.val);
    			res = res.next;
    			p2 = p2.next;
    		}
    		return a;
    	}
    
  • 025-复杂链表的复制

    题目不明确

  • 036-两个链表的第一个公共结点 — 比较重要

    这个也很简单,主要是明白他的特性,我发现做题就是这样的,只要明白他的特点,代码就相对好写一点。比如这里说的的公共节点一定是从这个节点开始,他后面的一模一样,那么从后开始循环,第一个一样的就是开始节点。

    主要方法:

    1. 两个stack,或者一个set+循环一个链表。
    2. 先求两个长度,然后长的先跑相差的长度,最后再一起跑
    3. 类似追赶问题,两个链表从头开始走,如果走到了末尾,就重新开始走,他们一定会相交于一点,可能是公共节点,可能是null;
    // 和环那个有点一样是追赶问题。
    // 两个链表一起跑,先跑完的在从头开始跑,这样总有一天他们会遇到相同的节点
    public static ListNode method(ListNode pHead1, ListNode pHead2) {
    		ListNode p1 = pHead1;
    		ListNode p2 = pHead2;
    		while(p1 != p2) {
    			if(p1 != null) p1 = p1.next;
    			if(p2 != null) p2 = p2.	next;
    			if(p1 != p2) {
    				if(p1 == null) p1 = pHead1;
    				if(p2 == null) p2 = pHead2;
    			}
    		}
    		return p1;
    } 
    
  • 055-链表中环的入口结点---- 比较重要

    这一题需要明白环是什么意思 1- 2- 3- 4- 7-3 这样就是环,同时要明白一个特点,因为它是个环,因此必定慢指针必定会追上快指针,追上之后,慢指针再从头开始走,必定会相交于环入口处

    	public ListNode method(ListNode pHead) {
            if(pHead == null || pHead.next == null || pHead.next.next == null) {
    			return null;
    		}
            
    		ListNode slow = p.next;
    		ListNode fast = p.next.next;
    		
    		while(slow != fast) {
    			slow = slow.next;
    			fast = fast.next.next;
    		}
    		slow = pHead;
    		while(slow != fast) {
    			slow = slow.next;
    			fast = fast.next;
    		}
    		return slow;
    	}
    
  • 056-删除链表中重复的节点

    首先他又没表达清晰,这个链表的重复节点是连续的。递归版本很厉害。简单的循环你也会。这个循环我没写出来,有点绕,要多练一下。

    	// 循环版本用一个record记录
    	public static ListNode method(ListNode pHead) {
    		ListNode p = pHead;
            ListNode record = p;
            ListNode res = record;
            while(p != null){
                boolean flag = false;
            	record = p;
                while(p.next != null && p.next.next != null 
                		&& p.next.val == p.next.next.val){
                    p = p.next.next;  // 应该跳到next.next
                    record.next = p;
                    flag = true;
                }
                if(!flag) {
                	p = p.next;
                	record.next = p;
                }
            }
            return res;
    	}
    
    	// 递归版本
    	if (p == null || p.next == null) { // 只有0个或1个结点,则返回
                return p;
        }
        if(p.val == p.next.val) {
            while(p != null && p.val == p.next.val) {
                p = p.next;
            }
            return method(p.next);
        } else {
            p.next = method(p.next);
            return p;
    	}
    

三、二叉树题

​ 二叉树的题几乎就是递归,因为有左右子树没法循环,只能递归

  • 004-重建二叉树

    Question:

    输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。 
    

    前序遍历一定是根节点在前面,所以其实你可以认为他的每个节点都是根节点,中序遍历确定root后前面的一定是左节点,右边一定为右节点,而且中序遍历root左节点的数量==前序遍历root节点后面的数量,因为前序遍历,第一个是root,后面一定先跟着左节点。另外一个左节点树都是相同遍历方法得到,因此是个递归问题。递归总结,如果有两个数组一般就不用上下边界

    public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
    		if(pre.length == 0) {
    			return null;        // 这个地方凭借思考去推觉得不对,但是直觉又告诉我肯定有个这个
    		}
    		if(pre.length == 1) {
    			return new TreeNode(pre[0]);
    		}
    		
    		int rootIndex = 0;
    		for (int i = 0; i < in.length; i++) {
    			if (pre[0] == in[i]) {
    				rootIndex = i;
                    break;
    			}
    		}
    		TreeNode root = new TreeNode(pre[0]);
    		root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, rootIndex+1), 		                                              Arrays.copyOfRange(in, 0, rootIndex));
    		root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, rootIndex+1,                                pre.length), Arrays.copyOfRange(in, rootIndex+1, in.length));
    		return root;
    	}
    
  • 017-树的子结构

    下面是我自己的思路写的,当然还是看了答案修改了farCompare这个方法,其实这个答案是有局限性的,是不对的。我主要是想总结一下递归的特点(递归总结)。下面farCompare方法我们用到了返回boolean类型,一般样式是,xxx情况返回true,xxx情况返回fasle,最后返回xx && yy 这种样式,这个我们在文章开头总结了一下。另外就是findSomeVal这个我们写的很垃圾,一般不会有a = findSomeVal(root.left, target); a = findSomeVal(root.rigth, target);这种同时存在,因为这样完全不知道要返回什么。参考上一题,通常样式是root.left = , root.right = 这种赋值操作,最后返回一个root,这个我们也在文章开头中总结了模板。

    // 正宗解法 
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
            boolean result = false;
            if(root2 == null || root1 == null) {
            	return false;
            }
            if(root1.val == root2.val){
                result = subMethod(root1,root2);
            } 
            if(!result) result = HasSubtree(root1.left,root2);
            if(!result) result = HasSubtree(root1.right,root2);
            return result;
        }
        
        public boolean subMethod(TreeNode root1,TreeNode root2){
            if(root1 == null && root2 != null){
                return false;
            }
            if(root1 == null && root2 == null){
                return true;
            }
            if(root2 == null){
                return true;
            }
            if(root1.val == root2.val){
                return subMethod(root1.left,root2.left) && subMethod(root1.right,root2.right)
            }
            return false;
    }
    
    	public static TreeNode some;
    	public static boolean HasSubtre(TreeNode root1,TreeNode root2) {
    		TreeNode a = findSomeVal(root1,root2.val);
    		if(a != null) {
    			return farCompare(a, root2);
    		}
    		return false;
    	}
    
    	public static TreeNode findSomeVal(TreeNode root, int target) {
    		TreeNode a;
    		if(root == null) {
    			return null;
    		}
    		if(root.val == target) {
    			return root;
    		}
    		a = findSomeVal(root.left, target);
    		if(a != null) {
    			return a;
    		}
    		a = findSomeVal(root.right, target);
    		if(a != null) {
    			return a;
    		}
    		return null;
    //		if(root != null) {   // 指的是这里! 一般是root.left = /root.right = 最后返回root
    //			if(root.val != target) {
    //				findSomeVal(root.left, target);
    //				findSomeVal(root.right, target);
    //			} else {
    //				some = root;
    //			}
    //		}
    	}
    	
    	public static boolean farCompare(TreeNode root1, TreeNode root2) {
    		if(root2 == null) {
    			return true;
    		}
    		if(root1 == null && root2 != null) {
    			return false;
    		}
    		if(root1.val != root2.val) {
    			return false;
    		}
    		return farCompare(root1.left, root2.left) && farCompare(root1.right, root2.right);
    //		if(root1 != null && root2 != null && root1.val == root2.val) {
    //			farCompare(root1.left, root2.left);
    //			farCompare(root1.right, root2.right);
    //			return true;   // 我一直在纠结怎么返回true,你看看上面那个返回 if(root2 == null) {return true;},我似乎有点感觉了
    //		} else {
    //			return false;
    //		}
    	}
    
  • 018-二叉树的镜像

    这个就是递归,而且很简单

    public static boolean method(TreeNode root) {
    		TreeNode r1 = root;
    		TreeNode r2 = root;
    		return subMethod(r1,r2);
    	}
    		
    	public static boolean subMethod(TreeNode root1, TreeNode root2) {
    		if(root1 == null && root2 != null) {
    			return false;
    		}
    		if(root1 != null && root2 == null) {
    			return false;
    		}
    		if(root1 == null && root2 == null) {
    			return true;
    		}
    		if(root1.val != root2.val) { 
    			return false;
    		}
    		return subMethod(root1.left, root2.right) && subMethod(root1.right, root2.left);
    	}
    
  • 022-从上往下打印二叉树

    这个也很简单,借助队列来实现,或者使用list.remove(0)这个也行,不用递归,循环就可以了

  • 023-二叉搜索树的后序遍历序列

    确认一个数组是否是后续遍历的结果,规律是最后一个节点一定是根节点,然后前面比他小的一定是他的左节点,比他大的一定是他的右节点。因此如果右边有比他小的那么就一定是不对的。

    这里我想递归总结一点,就是当你遇到有界限的时候,果断把边界放到递归参数中,而前后边界的关系就是递归的出口。而往往最后一个return就是xx && yy,对比二叉树第一题,他因为有两个数组,不好使用前后边界参数。

    	public static boolean VerifySquenceOfBST(int [] sequence,int start,int end) {
    		if(start >= end) {
    			return true;
    		}
    		int root = sequence[end];
    		int i = 0;
    		while(sequence[i] < root) {
    			i++;
    		}
    		int j = i;
    		while(j > root) {
    			if(sequence[j] < root) {
    				return false;
    			}
    			j++;
    		}
    		return VerifySquenceOfBST(sequence,start,j-1) 
    				&& VerifySquenceOfBST(sequence, j, end-1);
        }
    
  • 024-二叉树中和为某一值的路径

    首先这个路径代表从头到尾,而不是中间不完全的路径。其次我们以后遇到有求和的,我们要巧用target-xx这种样式,因为这样我们就只要比较当前值 和 差值是否相等就可以了。很巧妙。

    	static ArrayList<ArrayList<Integer>> res = new 	ArrayList<ArrayList<Integer>>();
    	public static ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
    		ArrayList<Integer> temp = new ArrayList<Integer>();
    		subMethod(temp,root,target);
    		return res;
    		
    	}
    	
    	private static void subMethod(ArrayList<Integer> temp, TreeNode root, int target) 	  {
    		temp.add(root.val);
    		// if的条件很重要,尤其是后面那两个 == null
    		if(root.val == target && root.left == null && root.right == null) {
    			res.add(new ArrayList <Integer>(temp));
    		//	return; 这里不用return
    		}
    		if(root.left != null) {
    			subMethod(temp,root.left,target - root.val);
    		}
    		if(root.right != null) {
    			subMethod(temp,root.right,target - root.val);
    		}
    		temp.remove(temp.size()-1);
    	}
    
  • 026-二叉搜索树与双向链表

    看不懂

  • 038-二叉树的深度

    这题比较比较不好理解,最小的情况是这样,如果有一个二叉树,只有左节点,没有右节点,那么我们先遍历左节点得到1,右节点得到0,使用本质就是比较一个节点左边有还是右边有节点,返回最大的那个值。因为这个方法不用开辟空间,因此要学会。这个递归太抽象了。

    public int TreeDepth(TreeNode root) {
    		if(root == null) {
    			return 0;
    		}
    		int left = TreeDepth(root.left) + 1;    
    		int right = TreeDepth(root.right) + 1;
    		return left >= right ? left : right;  // 相当于一个root节点下是否有左右节点,看谁比较大,谁大那么谁就是深度
    	}
    
  • 039-平衡二叉树

    看我们改进的代码,递归总结:通过返回一个特殊的东西,然后添加一个if判断,如果是特殊的,就继续返回特殊的,这样能提前终止递归;

    public int countFor(TreeNode root, int count){
    	if(root == null){
    		return -1;
        }
        if(count )
        count++;
        int left = countFor(root.left,count);
        int right = countFor(root.right,count);
        if(Math.abs(left - right) > 1){
            flag = false;
        }
        return Math.max(left,right);
    }
    
    // 看了其他人的代码 改进的:
    		if(root == null) {
    			return count; // 确实是,如果最后一个没有的话应该返回之前计算的长度
    		}
            if(count == -1){  // 判断是否是那个特殊的字符,这样能提前结束递归
                return -1;
            }
            count++;
    		int left = judgeIsBalance(root.left,count );
    		int right = judgeIsBalance(root.right, count);
    		if(Math.abs(left-right)>1) {
    			return -1;  // 返回一个特殊的字符
    		}
    		return Math.max(left, right);
    
  • 057-二叉树的下一个结点

    无语的题目,表述不清,没意思;

     public TreeLinkNode GetNext(TreeLinkNode node)
        {
            if(node==null) return null;
            if(node.right!=null){    //如果有右子树,则找右子树的最左节点
                node = node.right;
                while(node.left!=null) node = node.left;
                return node;
            }
    
            while(node.next!=null){ //没右子树,则找第一个当前节点是父节点左孩子的节点
                if(node.next.left==node) return node.next;
                node = node.next;      
            }
            return null;   //退到了根节点仍没找到,则返回null
            
        }
    
  • 058-对称的二叉树

    经典的递归样式,不管你传left还是right 我就是比较左val 是否 等于 右val

    public boolean mirro(TreeNode root1,TreeNode root2){
        if(root1 == null && root2 != null){
            return false;
        }
        if(root2 == null && root1 != null)
        {
    		return false;        
        }
        if(root2 == null && root1 == null){
            return true;
        }
        if(root2.val != root2.val){
            return false;
        }
        return mirro(root1.left,root2.right) && mirro(root2.left,root1.right);
    }
    
  • 059-按之字形顺序打印二叉树

    这个题很简单了,就是两个stack,奇数行怎么存进去,偶数行怎么存进去。

  • 062-二叉搜索树的第k个结点

    这个我们能反映过来是中序遍历。递归总结:TreeNode node = KthNode(pRoot.left,k);不要迟疑直接先写,然后下面的 if(node != null){return node;} 记住这个模式 最后返回return null,记住这个模式!

    	int index = 0;
        TreeNode KthNode(TreeNode pRoot, int k)
        {
            if(pRoot == null){
                return null;
             }
             TreeNode node = KthNode(pRoot.left,k);
             if(node != null){
                 return node;
             }
             index ++;
              if(index == k){
                  return pRoot;
              }
              node = KthNode(pRoot.right,k);
            if(node != null){
                 return node;
             }
            return null;
        }
    
  • 064-滑动窗口的最大值(双端队列)

    其实本质是构造一个单调递减的队列

    /**
     * f滑动窗口
     * r始终保持队头元素最大,然后后面的元素单调递减
     * r因此就有,如果新来的元素比队尾大,就移除队尾的元素---这样就会导致可能比队头的还要大,把队头也给移除了,是正确的
     * r如果新来的比对尾小,那就就放入队尾
     * r判断队头的元素是否不应该在队列中,不实的话就移除
     * r例如 2,3,4,2,6,2,5,1
     * 2
     * 3
     * 4
     * 4 2 
     * 6
     * 6 2 
     * 6 5
     * 5 1  --- 这里就是计算出6是不属于队列的
     * @author fongfiafia
     */
    public class Code064 {
    
    	public ArrayList<Integer> maxInWindows(int[] num, int size) {
    		ArrayList<Integer> res = new ArrayList<>();
    		if (size == 0)
    			return res;
    		Deque<Integer> q = new LinkedList<>();
    
    		for (int i = 0; i < num.length; i++) {
    			while (!q.isEmpty() && num[q.getLast()] < num[i]) {
    				q.pollLast();
    			}
    			q.addLast(i); // 这里如果进入了上面的循环 addlast 就等于addfirst
    			while (!q.isEmpty() && q.getFirst() <= i - size) {
    				q.pollFirst();
    			}
    			if (i >= size - 1) {
    				res.add(num[q.getFirst()]);
    			}
    		}
    		return res;
    	}
    }
    
    
  • 034-第一个只出现一次的字符

  • public char method(String str){
    	int[] temp = int[56];
        for(int i = 0; i < str.length; i++){
            temp[(int)str.charAt(i)]+=1;
        }
         for(int i = 0; i < str.length; i++){
             if(temp[(int)str.charAt(i)] - 56 == 1){
                 return str.charAt(i);
             }
        }
    }
    

四、动态规划

为了快速学习,我参考了动态规划 这位的动态规划思路,这样上手快一点。

动态规划的核心思想是:数学归纳法。有点忘记了什么是数序归纳法,举个例子你就知道了:

证明:S(n) = 1 + 2 + 3  ….  + n 前n项和为n(n + 1) / 2

n = 1, S(1)  = 1

假设n时命题成立 ----------假设某个时候成立,证明他后面的是否也成立

N+ 1时,

S(n  + 1) 

	=  S(n) + n + 1

	= n(n + 1)/2 + n + 1

	= (n + 1)(n + 2)/ 2

因此成立

  • 问题一:
    在这里插入图片描述

为什么会想到他是动态规划呢?emm,我还没搞明白,就目前我们掌握的,递归,循环,二分似乎都用不上,那么就只剩下动态规划?暂时先这么想吧。

根据那篇文章我记录一下体会:

第一步:动态规划一定有一个数组dp[],我们首先就要想好这个数组表达什么意思,其实很明显,这个数组应该是:dp[5] 表示num[5] 他的最长上升序列长度是多少?因此我们的最终返回结果应该是:

for (int i = 0; i < dp.length; i++) {
        res = Math.max(res, dp[i]);
    }

第二步:dp[5]这个值怎么算的呢?这当然才是关键,而你的思考应该是dp[5]怎么通过dp[0]----dp[4]的来的到dp[5]。很明显的其实应该是index = 5这个前面第一个小于他的index 对应的dp[index] + 1 就是 dp[5] 的值。当然了这里他用的从头开始比较:–我要试验一下我的方法,当然不影响复杂度其实。但是他这个思想我认为是最朴素的,最暴力的,我觉得一开始可以按他这么来想。其次这个模式: dp[i] = Math.max(dp[i], dp[j] + 1); 也是常用模式。

for (int j = 0; j < i; j++) {
    if (nums[i] > nums[j]) 
        dp[i] = Math.max(dp[i], dp[j] + 1);
}

更进一步的,所有dp[n]都可以写出来:

for (int i = 0; i < nums.length; i++) {
    for (int j = 0; j < i; j++) {
        if (nums[i] > nums[j]) 
            dp[i] = Math.max(dp[i], dp[j] + 1);
    }
}

第三步:细节dp[n]应该要初始化大小为1,这个初始化也是动态规划必须的动作。

验证一下我的想法,也就是从n-1开始比较,不要再从头比较了:这样是正确的,当然其实复杂度不会变。

			for(int j = n-1; j >= 0; j--) {
				if(num[n] >= num[j]) {
					dp[n] = dp[j] + 1;
					break;
				}
			}
  • 问题二:凑硬币:给你 k 种面值的硬币,面值分别为 c1, c2 ... ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。算法的函数签名如下:

    解答:根据上面的三步走:

    1、确定dp[],以及他的含义。这里的话我们第一次想错了,其实也不应该错,根据【状态】这个东西,我们知道不会变得应该是amount,目标值,他应该就是状态,也就是dp[]应该是:目标金额为n时,需要dp[n]个硬币。对比上面子序列,上面是:当num下标为n时,他的最长字串的dp[n],可以看出你的最后结果都是dp[],只不过,dp的下标代表不同含义。

    2、分解子问题,已知dp[0-----n-1],怎么得到dp[n]呢?,思考得到,如果当前硬币面额是3元,我要求dp[10],那么就一定是dp[7] + 1,所以需要遍历coins数组

    			for(int coin : coins) {
    				if((n - coin) < 0) {
    					continue;
    				}
    				dp[n] = Math.min(dp[n], dp[n-coin] + 1); // 我要求8元需要的硬币数量,并且还有3元硬币可以选择,那么一定是5元需要的数量 + 1
    			}
    

    3、给基本情况+初始化。dp[0] = 0,这里总结,如果求最大值,给dp初始化0,如果求最小值,给dp初始化最大值。

    	public static int coinChange(int[] coins, int amount) {
    		// 我们定义dp是对应那个下标的coins 的数量 -- 这样定义不对,这样定义我们没法进行下一步
    		// 应该定义为当前的目标金额是 n,至少需要 dp(n) 个硬币凑出该金额。
    		int[] dp = new int[amount+1];
    		Arrays.fill(dp, amount+1); // 这个初始化有讲究的,相当于是给个最大值(可以记住是 求最小										值是初始化正无穷,求最大值时候初始化0)
    		dp[0] = 0; // 给个基本情况
    		for(int n = 0; n < amount+1 ; n++) {
    			for(int coin : coins) {
    				if((n - coin) < 0) {
    					continue;
    				}
    				dp[n] = Math.min(dp[n], dp[n-coin] + 1); 
    			}
    		}
    		return dp[amount] == amount + 1? -1 : dp[amount];
    	}
    
  • 问题三:给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?

    也是三步走

    1、构建数组,这里明显有两个状态因此就是大胆的给他定为二维数组:dp[][];但是关键还是要定义出这个二维数组是表示什么含义,应该是dp[i][w] = val 表示:前i个物品,在背包容量为w的时候,最多放价值val。总结:我们可以看到dp数组的值一定是我们最后要的东西,比如最长序列是序列长度,硬币问题是硬币数量。其次就有两个遍历for。

    2、分解子问题,已知xx,求dp[i][w] 。思考的方向其实和硬币的一样,就是w-遍历物品的重量,然后就是前一个val+减去遍历物品的val,表达式是:dp[i][w] = dp[i-1][w-weight[i-1]] + val(i)。然后他有个选择问题,就是要不要把第i个物品放进去,如果不放那么就是dp[i-1][w]。综合到代码就是:

    // w是要求小于的重量  n是几个物品
    	public static int method(int w, int n, int[] wt,int[] val) {
    		// 对于前i个物品,放在容量为w的背包中,价值dp[i][w]
    		int[][] dp = new int[n+1][w+1];
    		for(int i = 1; i < n+1; i ++) {
    			for(int j = 1; j < w+1; j++) {
    				if(j - wt[i-1] < 0) {
    					dp[i][j] = dp[i-1][j]; // 为什么是j-1,只是因为j是从1开始的,j-1就是j
    				} else {
    					dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j - wt[i-1]] + val[i-1]); // 这个j - wt[i-1]和凑硬币是一样的
    				}
    			}
    		}
    		return dp[n][w];
    	}
    

能装的价值是多少?

也是三步走

1、构建数组,这里明显有两个状态因此就是大胆的给他定为二维数组:dp[][];但是关键还是要定义出这个二维数组是表示什么含义,应该是dp[i][w] = val 表示:前i个物品,在背包容量为w的时候,最多放价值val。总结:我们可以看到dp数组的值一定是我们最后要的东西,比如最长序列是序列长度,硬币问题是硬币数量。其次就有两个遍历for。

2、分解子问题,已知xx,求dp[i][w] 。思考的方向其实和硬币的一样,就是w-遍历物品的重量,然后就是前一个val+减去遍历物品的val,表达式是:dp[i][w] = dp[i-1][w-weight[i-1]] + val(i)。然后他有个选择问题,就是要不要把第i个物品放进去,如果不放那么就是dp[i-1][w]。综合到代码就是:

// w是要求小于的重量  n是几个物品
	public static int method(int w, int n, int[] wt,int[] val) {
		// 对于前i个物品,放在容量为w的背包中,价值dp[i][w]
		int[][] dp = new int[n+1][w+1];
		for(int i = 1; i < n+1; i ++) {
			for(int j = 1; j < w+1; j++) {
				if(j - wt[i-1] < 0) {
					dp[i][j] = dp[i-1][j]; // 为什么是j-1,只是因为j是从1开始的,j-1就是j
				} else {
					dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j - wt[i-1]] + val[i-1]); // 这个j - wt[i-1]和凑硬币是一样的
				}
			}
		}
		return dp[n][w];
	}

3、数据初始化,其实应该是第一行和第一列为0,但是数组已经帮忙我们全部定义为0了

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值