LeetCode 剑指offer 简单篇(更新中)
我有信心,这篇帖子一定连更,绝对不鸽!!
5.6 元气满满第一天
[ 剑指offer03. 数组中重复的数字 ]
class Solution { public int findRepeatNumber(int[] nums) { //我的的思路:把nums放进hash表中,nums[i]是否在hash表中,在的话,返回nums[i] //知识点:关于hashset和hashmap //HashMap实现了Map接口 HashSet实现了Set接口 //HashMap储存键值对 HashSet仅仅存储对象 //使用put(key,value)方法将元素放入map中 使用add(value)方法将元素放入set中 //不允许key重复,但允许value重复 不允许value重复,重复元素放不进去 //在hashmap中 //获取key所对应的value值,没有就获取defaultValue;hash.getOrDefault(key,defaultValue) //判断是否包含元素是由containsKey() //hashSet判断是否包含元素使用 contains() Set<Integer> hash=new HashSet<Integer>(); hash.add(nums[0]); for(int i=1;i<nums.length;i++){ if(hash.contains(nums[i])){ return nums[i]; }else{ hash.add(nums[i]); } } return -1; } }
复杂度分析:
时间复杂度:遍历数组O(n),向hash中添加元素O(1),所以时间复杂度O(n)
空间复杂度:不重复的元素每个都有可能存入集合,O(n)
[ 剑指offer 05.替换空格 ]
class Solution { public String replaceSpace(String s) { //K神思路:在java中 String是字符串常量,不可变对象,无法直接修改字符串的某一位,需要新建一个可以修改的字符串对象进行替换。遍历字符串,放入res中,如果某个字符为' ',append("%20") //知识点: //关于StringBUffer 和StringBuilder //StringBuffer字符串变量,线程安全,支持多线程,若需要经常对字符串进行改变,用它,转换为string对象使用toString()方法,append(E)在末端添加元素,insert(index,E)在指定地点添加元素。相当于全局变量。 //StringBuilder 一般用在内部,线程不安全,相当于'+',用完可以丢弃。 //关于char和character //char是基本数据类型,Character是其包装器类,实例化出来的叫对象。 StringBuilder sb=new StringBuilder(); for(int i=0;i<s.length();i++){ char c=s.charAt(i); if(c==' '){ sb.append("%20"); }else{ sb.append(c); } } return sb.toString(); } }
复杂度分析:
时间复杂度:遍历字符串O(n),向sb后边添加字符O(1),所以时间复杂度为O(n)
空间复杂度:一个可变的字符串,最多可能存放3n个元素,空间复杂度O(n)。
[ 剑指Offer 06.从尾到头打印链表 ]
方法一:辅助栈
class Solution { public int[] reversePrint(ListNode head) { //我的思路:一涉及到反转,一般都会用到栈这个数据结构,后进先出。 Deque <Integer> stack=new LinkedList<>(); while(head!=null){ stack.push(head.val); head=head.next; } int []res=new int[stack.size()]; for(int i=0;i<res.length;i++){ res[i]=stack.pop(); } return res; } }复杂度分析:
时间复杂度:两次循环,出栈入栈共使用O(n)
空间复杂度:既有构造stack,也有构造数组,O(n)
方法二:不用栈不递归,怎样都是扫描两遍,不额外消耗空间倒叙存放,直接倒着构造所需返回的数组即可。
方法二:不用栈不递归,怎样都是扫描两遍,不额外消耗空间倒叙存放,直接倒着构造所需返回的数组即可。
class Solution { public int[] reversePrint(ListNode head) { //把head当哨兵,让node去走循环,得到链表的长度len ListNode node=head; int len=0; while(node!=null){ node=node.next; len++; } //定义res,倒叙构造res int [] res=new int[len]; for(int i=len-1;i>=0;i--){ res[i]=head.val; head=head.next; } return res; } }
复杂度分析:
时间复杂度:两次循环O(n)
空间复杂度:构造数组O(n)
[ 剑指Offer 09.用两个栈实现队列 ]
这题拿捏住一个关键:删除元素的原则:只有当删除栈为空时,才把输入栈的元素导入。
class CQueue { //我的思路:先定义两个栈,栈是后进先出的,实现队列,先进先出,前出后进,然后实现插入删除方法 //不要直接用stack做,速度慢。原因的话是Stack继承了Vector接口,而Vector底层是一个Object[]数组,那么就要考虑空间扩容和移位的问题了。 //可以使用LinkedList来做Stack的容器,因为LinkedList实现了Deque接口,所以Stack能做的事LinkedList都能做,其本身结构是个双向链表,扩容消耗少。 //s1,s2放外边大家都能用 Deque<Integer> s1; Deque<Integer> s2; public CQueue() { //实例化对象 s1=new LinkedList<>(); s2=new LinkedList<>(); } public void appendTail(int value) { //放入第一个栈 s1.push(value); } public int deleteHead() { /*删除元素的原则:只有当输出栈为空时候,才将输入栈的元素导入,否则先输出输出栈中的元素 */ //当输出栈为空,且输入栈不为空时 if(s2.isEmpty()&& !s1.isEmpty()){//判断栈空的方法isEmpty() while(!s1.isEmpty()){ s2.push(s1.pop()); } return s2.pop(); }else if(s1.isEmpty() && s2.isEmpty()){//输出栈为为空,输入栈为空 return -1; }else {//输出栈不为空 return s2.pop(); } } }
第一次做时候的一些小笔记:其实以前做的就很好了,一定多多反思总结。
class CQueue { // 栈(stack):先进后出 // 队列(queue):先进先出 // Deque接口,是queue接口的子接口,是指队列两端的元素,既能入队(offer)也能出队。 // 如果将Deque限制为只能从一端进行入队,和出队,就是栈的数据结构的实现。对于栈而言,有入栈(push)和出栈(pop),遵循先进后出的规则 //Java的集合类并没有单独的Stack接口,因为有个遗留类名字就叫Stack,出于兼容性考虑,所以没办法创建Stack接口,只能用Deque接口来“模拟”一个Stack了。 // 当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,那样会破坏栈的本质。 // 用两个栈实现队列时,使用deque // 这道题中定义栈的方法 // Deque <Integer> stack=new LinkedList<>(); // java语言不使用这种方法普通定义栈的方法 // Stack<Integer> stack=new Stack<>() // 普通定义队列的方法 // Queue <Integer> queue=new LinkedList<>(); // 这道题的解题思路 // 两个栈s1,s2 因为栈的特性是先进后出,队列的特性是后进先出 // 一个栈s1实现插入,一个栈s2实现删除 // s1直接入栈 s1.push()一个元素 // s2删除元素,s1入栈直接压到栈底了,所以会后出,将s1中的元素一个个的pop到s2中,后进s1的元素就被压到s2栈底了 // 有一个问题是,当s2中有元素,又向s1push 了新的元素,但是按理说应该s2中的元素按先进先出的规则先走,要注意的是,当s2不为空时,s1不许向里压栈,只有s2空了,才允许向里填s1中的所有元素 // 判断为空s.isEmpty(),是true,否false // 定义两个栈 //定义两个公共变量s1,s2 Deque<Integer> s1; Deque<Integer> s2; public CQueue(){ s1=new LinkedList<>(); s2=new LinkedList<>(); } public void appendTail(int value){ s1.push(value); } public int deleteHead(){ //当s2不为空时 //栈的判空isEmpty() if(!s2.isEmpty()){ return s2.pop(); } //当s2为空时 当s1不为空时 if(s2.isEmpty() &&!s1.isEmpty() ){ while(!s1.isEmpty()){ s2.push(s1.pop()); } return s2.pop(); } //当s1s2为空,返回-1 return -1; } }复杂度分析:
时间复杂度:appendTail()的只需要一次添加元素,时间复杂度为O(1);deleteHead()在最坏情况下输出栈可能要添加N个元素,所以时间复杂度为O(n)。
空间复杂度:在最差情况下,输入输出栈可能要添加N个元素。
5.8
[ 剑指Offer 10-Ⅰ.斐波那契数列 ]
class Solution { public int fib(int n) { //我的思路:迭代的思路在里边,最后一个元素的值与前两个元素的有关,那么我把前俩个元素的值存起来即可。 //特殊情况先输出 if(n==0){ return 0; } if(n==1 || n==2){ return 1; } int [] res=new int[3]; res[0]=0; res[1]=1; res[2]=1; for(int i= 3;i<=n;i++){ res[0]=res[1]; res[1]=res[2]; res[2]=(res[0]+res[1])%1000000007;//记得题目要求的取余 } return res[2]; } }
复杂度分析:
时间复杂度:f(n)需要进行n次循环,O(N)
空间复杂度:开辟了一个长度为常数的数组,O(1)
以前的思路:
class Solution { public int fib(int n) { //方法一、动态规划 if(n==0){ return 0; } if(n==1){ return 1; } //字符串长度是可以变化的 int [] dp= new int [n+1]; dp[0]=0; dp[1]=1; //取余符号% for(int i=2;i<=n;i++){ dp[i]=(dp[i-1]+dp[i-2])%(1000000007); } return dp[n]; // //方法二、递归 // //如果n=1或者2时 // if(n==1 || n==2){ // return 1; // } // //把最开始两个数组存储起来 // int a=0,b=1; // int ans=0; // for(int i=2;i<=n;i++){ // ans=(a+b)%(1000000007); // a=b; // b=ans; // } // return ans; } }[ 剑指Offer 10-Ⅱ.青蛙跳台阶问题 ]
这题跟上边异曲同工
class Solution { public int numWays(int n) { //我的思***蛙的最后一跳有两种情况,跳一阶或者跳两阶,所以只要求出青蛙跳倒数第一阶之前的方法+跳倒数第二阶之前的方法之和,即可获得青蛙跳台阶的方法。还是一个递归的问题,最后一跳需要前边跳法递归而来。f(n)=f(n-1)+f(n-2),跟斐波那契数列一模一样。 int []dp=new int[n+1];//把0阶台阶也算上,对应着好看 if(n==0||n==1){ return 1; } //0阶台阶,1种;一阶台阶,1中;二阶台阶,2种 dp[0]=0; dp[1]=1; dp[2]=2; for(int i=3;i<=n;i++){ dp[i]=(dp[i-1]+dp[i-2])%1000000007; } return dp[n]; } }
复杂度分析:
时间复杂度:f(n)需要进行n次循环,O(N)
空间复杂度:开辟了一个长度为n+1数组,O(n)
5.9 不要偷懒
[ 剑指Offer 11.旋转数组的最小数字 ]
class Solution { public int minArray(int[] numbers) { //思路:旋转数组最开始是一个升序数组,当这个数组整个进行旋转时返回原数组。第一个值就是最小值(特殊情况)。 //部分旋转:当后边的一个值小于前边的值时,说明后边那个值就是最小值,返回后边那个值即可。 int res=-1; //部分旋转 for(int i=0;i<numbers.length-1;i++){ if(numbers[i]>numbers[i+1]){ res=numbers[i+1]; return res; } } //完全旋转 return numbers[0]; } }复杂度分析:
时间复杂度:遍历数组,O(N)
空间复杂度:只用了1个变量,O(1)
官方、K、liweiwei都说二分法,贴一个二分的上来
class Solution { public int minArray(int[] numbers) { //二分法思路:双指针i,j指向数组两端,其中mid=i+(j-i)/2。 //当numbers[mid]>numbers[j]时,m在大数组中,所求最小的值在[mid+1,j]中,令i=mid+1 //当numbers[mid]<numbers[j]时,m在小数组中,所求最小值在[i,m],令j=mid; //当numbers[mid]=numbers[j]时,最小值不能确定在哪边,比如[5,4,4,4,4]和[1,2,2,2,2],执行j=j-1,打破这个平衡,然后继续比较。 //当i=j时跳出循环,返回numbers[j]即可。 int i=0,j=numbers.length-1; while(i<j){ int mid=i+(j-i)/2;//如果是(i+j)/2,可能会出现越界问题 if(numbers[mid]>numbers[j]){ i=mid+1; }else if(numbers[mid]<numbers[j]){ j=mid; }else{ j=j-1; } } return numbers[j]; } }复杂度分析:
时间复杂度: 二分法,没有完全的遍历数组 O(logN) 。在数组全部旋转情况下,为O(N)
空间复杂度:i,j,m使用常数个额外空间 O(1)
5.10 新到的这个组可真卷呀,快乐的摸鱼时光一去不复返了~
[ 剑指Offer 15.二进制中1的个数 ]
public class Solution { // you need to treat n as an unsigned value public int hammingWeight(int n) { //思路:题干说32位的二进制数,遍历这个二进制数32次,用二进制数最后一位进行异或操作,如果等于1,cnt++;二进制数进行右移,进行下次循环 int cnt=0; for(int i=0;i<32;i++){ if((n&1)==1){//判断当前位是否为1,&运算都为1才等于1 cnt++; } //n进行右移1位,比较下一位,前边缺少的用0补上 n=n>>>1; } return cnt; } }
以前的做法:
public class Solution { // you need to treat n as an unsigned value public int hammingWeight(int n) { //二进制中的与 或 非 异或 //与& 全1则1 //或| 有1则1 //非~ 按位取反 ~10000 =01111 //异或^ 相同取0,不同取1 //位移运算:常见有左位移(<<<),与右位移(>>>)简单说就是将一个二级制数中的1向左或向右移动若干位,多余的位用0补齐 //若 n& 1 = 0,则 n 二进制 最右一位 为 0; //若 n & 1 = 1,则 n 二进制 最右一位 为 1 。 //&只能比较最后一位的,然后需要进行右移,移过的位置用0补齐 int cnt=0; for(int i=0;i<32;i++){ //用小括号括起来 if((n &1) ==1){ cnt++; } //比较完一次之后,将n右移一位,比较下一位,格式如下 n=n>>>1; } return cnt; } }复杂度分析:
时间复杂度:O(K),K为二进制数的位数,32
空间复杂度:O(1),我们只需要常数的空间保存若干变量。
[ 剑指Offer 17.打印从1到最大的n位数 ]
这题学到一个函数Math.pow(底数,次方数),这个数出来是double类型,注意转换类型
class Solution { public int[] printNumbers(int n) { //Math.pow(底数,几次方) // 如:int a=3; //int b=3; //int c=(int)Math.pow(a,b); //就是3的三次方是多少;a、b及函数值都是double型,强制装换成int类型的 int len=(int)Math.pow(10,n)-1; int [] res=new int[len]; for(int i=0;i<len;i++){ res[i]=i+1; } return res; } }复杂度分析:
时间复杂度:O(10^n) : 生成长度为 10^n 的列表需使用 O(10^n) 时间。
空间复杂度:列表的长度固定,需要O(1)大小的额外空间。