算法 | 一周刷完《剑指Offer》 Day1:第1~16题

开个新坑,准备校招研发岗面试,基本的算法还是要过关的。


写在前面

下一篇:算法 | 一周刷完《剑指Offer》 Day2:第17~26题


Day1:第1~16题

后面总会遇到难的绕的题,前两天多做几道,总不会亏的。

  • T1. 二维数组中的查找
  • T2. 替换空格
  • T3. 从尾到头打印链表
  • T4. 重建二叉树
  • T5. 用两个栈实现队列
  • T6. 旋转数组的最小数字
  • T7. 斐波那契数列
  • T8. 跳台阶
  • T9. 变态跳台阶
  • T10. 矩阵覆盖
  • T11. 二进制中 1 的个数
  • T12. 数值的整数次方
  • T13. 调整数组顺序使奇数位于偶数前面
  • T14. 链表中倒数第 K 个结点
  • T15. 反转链表
  • T16. 合并两个排序的链表

T1. 二维数组中的查找
题目描述

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

解题思路

因为矩阵中的每一个数,左边都比它小,下边都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。

	public boolean Find(int target, int[][] array) {
        if(array == null || array.length == 0 || array[0].length == 0) {
            return false;
        }
        
        int m = 0, n = array[0].length - 1;//从右上角开始找,array[0][n-1]
        
        while(m <= array.length - 1 && n >= 0) {
            if(target == array[m][n])
                return true;
            else if(target > array[m][n])
                m ++;
            else
                n --;
        }
        
        return false;
    }
T2. 替换空格
题目描述

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy,则经过替换之后的字符串为We%20Are%20Happy。

解题思路

由于每个空格替换成了三个字符,首先要扩容字符串长度至替换后的长度,因此当遍历到一个空格时,需要在尾部填充两个任意字符。

然后声明两个下标,一个为原字符串末尾下标 i,一个为现字符串末尾 j,两个下标同步从后往前扫。当 i 指向空格时,j 就倒着依次添加 ‘0’, ‘2’, ‘%’。

这样做的目的是: j 下标不会超过 i 下标,不会影响原字符串的内容。

	public String replaceSpace(StringBuffer str) {
		int oldLen = str.length();
		
		for(int i = 0; i < oldLen; i ++) {
			if(str.charAt(i) == ' ') {//每出现一个空格,在结尾添加两个任意字符,扩充字符串长度
				str.append("12");
			}
		}
		
		int i = oldLen - 1;
		int j = str.length() - 1;
		
		while(i >= 0 && j > i) {
			char c = str.charAt(i);
			i --;
			
			if(c == ' ') {//每出现一个空格,替换为%20
				str.setCharAt(j, '0');
				j --;
				str.setCharAt(j, '2');
				j --;
				str.setCharAt(j, '%');
				j --;
			} else {//否则照旧
				str.setCharAt(j, c);
				j --;
			}
		}
		
		return str.toString();
	}
T3. 从尾到头打印链表
题目描述

输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。

解题思路

使用栈实现:先入后出。全部入栈,依次出栈,顺序即为从尾到头。

	public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
		//使用栈实现,先入后出
		Stack<Integer> stack = new Stack<>();
		ArrayList<Integer> arrayList = new ArrayList<>();
		
		while(listNode != null) {
			stack.push(listNode.val);
			listNode = listNode.next;
		}
		
		while(!stack.isEmpty()) {
			arrayList.add(stack.pop());
		}
		
		return arrayList;
	}

使用递归实现:先加入链表后面的结点,再加入当前结点,顺序即为从尾到头。

	public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
		//使用递归实现,先加入链表后面的结点,再加入当前结点
	    ArrayList<Integer> arrayList = new ArrayList<>();
	    
	    if(listNode != null) {
	        arrayList.addAll(printListFromTailToHead(listNode.next));//先递归
	        arrayList.add(listNode.val);//再加入当前
	    }
	    
	    return arrayList;
	}
T4. 重建二叉树
题目描述

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

解题思路

首先清楚以下性质:
前序遍历:根 -> 左 -> 右,中序遍历:左 -> 根 -> 右。

因此一颗二叉树中,前序遍历第一个结点为根结点,通过它在中序遍历中的位置,可以将中序遍历分为两部分,左半部分是该结点的左子树右半部分是该结点的右子树

根据这个原理,递归执行即可重构出二叉树。

	private Map<Integer, Integer> map = new HashMap<>();//用于查找中序遍历数组中,每个值对应的索引
	
	public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
	    for(int i = 0; i < in.length; i ++) {
	    	map.put(in[i], i);//key: in的值,value: 值在in中位置
	    }
		
		return getBiTree(pre, 0, pre.length - 1, 
							in, 0, in.length - 1);
	}
	
	private TreeNode getBiTree(int[] pre, int preLeft, int preRight, //前序遍历及当前在前序遍历中的区间
								int[] in, int inLeft, int inRight) { //中序遍历及当前在前序遍历中的区间
	    if(preLeft == preRight) { //即根据前序遍历,当前结点无子结点
	    	return new TreeNode(pre[preLeft]);
	    }
	    if(preLeft > preRight || inLeft > inRight) {
	    	return null;
	    }
	    
	    TreeNode root = new TreeNode(pre[preLeft]);
	    int inIndex = map.get(root.val);
	    int leftTreeSize = inIndex - inLeft;//该结点左半部分的结点数
	    
	    //递归,获取左子树及右子树
	    root.left = getBiTree(pre, preLeft + 1, preLeft + leftTreeSize, //左半部分在前序遍历中的区间
	    						in, inLeft, inIndex - 1);//左半部分在中序遍历中的区间
	    root.right = getBiTree(pre, preLeft + 1 + leftTreeSize, preRight, //右半部分在前序遍历中的区间
	    						in, inIndex + 1, inRight);//右半部分在中序遍历中的区间
	    
	    return root;
	}
T5. 用两个栈实现队列
题目描述

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

解题思路

队列:先进先出,:先进后出

例如,假设 1 ~ n 的数字顺序,入Stack1,出的顺序为 n ~ 1 。

这时,Stack1出栈进入Stack2,进入Stack2的顺序为n ~ 1,那么Stack2出的顺序为 1 ~ n 。

相当于队列 1 ~ n 进, 1 ~ n 出。

	Stack<Integer> stack1 = new Stack<Integer>();//stack1入栈时使用
    Stack<Integer> stack2 = new Stack<Integer>();//stack2出栈时使用,直接出栈即可
    
    public void push(int node) {
        while(!stack2.isEmpty()) {//先将stack2中所有元素压入stack1
        	stack1.push(stack2.pop());
        }
        stack1.push(node);//然后将当前要进入队列元素压入stack1
        while(!stack1.isEmpty()) {//最后将stack1所有元素压入stack2
        	stack2.push(stack1.pop());//此时stack2中出栈顺序即为队列出栈顺序,相当于先入先出
        }
    }
    
    public int pop() {
    	return stack2.pop();
    }
T6. 旋转数组的最小数字
题目描述

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

解题思路

我们可以认为数组分成了两部分,前半部分是较大的数,后半部分是较小的数

利用二分查找的思路,观察取中的数在前半部分还是后半部分,以此来找出最小的数(后半部分的第一个数)。

	public int minNumberInRotateArray(int[] array) {
	    if(array.length == 0) {
	    	return 0;
	    }
	    
	    //二分查找
	    int low = 0, high = array.length - 1;
	    while (array[low] >= array[high]) {
	        if(high - low == 1) {
	        	return array[high];
	        }
	        int mid = low + (high - low) / 2;//取中
	        if(array[mid] >= array[low]) {//此时mid在前半区大的数里
	        	low = mid;
	        } else {//此时mid在后半区小的数里
	        	high = mid;
	        }
	    }
		return array[low];
    }
T7. 斐波那契数列
题目描述

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39

解题思路

斐波那契数列:F[n]=F[n-1]+F[n-2] (n>=3,F[1]=1,F[2]=1)

注意:此题要求F[1]=0。

	public int Fibonacci(int n) {//n<=39
		int[] array = new int[40];
		array[0] = 0;
		array[1] = 1;
		for(int i = 2; i < array.length; i ++) {
			array[i] = array[i - 1] + array[i - 2];
		}
		
		return array[n];
    }
T8. 跳台阶
题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

解题思路

动态规划的思想。这里采用倒着往前推的方法递减target,最后一次跳1或最后一次跳2,倒着往前递归。

	public int JumpFloor(int target) {
		if(target == 0) {
			return 0;
		}
		if(target == 1) {
			return 1;
		}
		if(target == 2) {
			return 2;
		}
		if(target > 2) {//递归求可能出现的情况总数(最后一次跳1或最后一次跳2,倒着往前推)
			return JumpFloor(target - 1) + JumpFloor(target - 2);
		}
		
		return 0;
    }

或者,其本质上是斐波那契数列的原理。

	public int JumpFloor(int target) {
		if(target == 0) {
			return 0;
		}
		if(target == 1) {
			return 1;
		}

		int[] array = new int[target];
	    array[0] = 1;
	    array[1] = 2;
	    for (int i = 2; i < target; i++) {
	        array[i] = array[i - 1] + array[i - 2];
	    }
	    return array[target - 1];
	}
T9. 变态跳台阶
题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

解题思路

同理,动态规划的思想,递归求解。(这次情况有点多,用到循环了)

	public int JumpFloorII(int target) {
		if(target == 0 || target == 1) {
			return 1;
		}
		if(target == 2) {
			return 2;
		}
		
		int sum = 0;
		for(int i = 0; i < target; i ++) {//本次跳0次到跳target-1次
			sum += JumpFloorII(i);//对于本次的跳跃又有多少种跳法?递归获取结果
		}
		
		return sum;
    }
T10. 矩阵覆盖
题目描述

我们可以用 2 * 1 的小矩形横着或者竖着去覆盖更大的矩形。请问用n个 2 * 1 的小矩形无重叠地覆盖一个 2 * n 的大矩形,总共有多少种方法?

解题思路

原理同T8,详见图片。

	public int RectCover(int target) {
		if(target == 0) {
			return 0;
		}
		if(target == 1) {
			return 1;
		}

		int[] array = new int[target];
	    array[0] = 1;
	    array[1] = 2;
	    for (int i = 2; i < target; i++) {
	        array[i] = array[i - 1] + array[i - 2];
	    }
	    return array[target - 1];
    }
T11. 二进制中 1 的个数
题目描述

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

解题思路

Integer.bitCount(n):技术整型的二进制表示中1的个数。。。

	public int NumberOf1(int n) {//(???)
		return Integer.bitCount(n);
    }

正常算法:&按位与。每次将 n 和 n-1 进行 & 运算,从右往左去掉二进制最右边的一个1。例如:

n:           100100
n - 1:       100011
n & (n-1):   100000

一次运算后,n由100100变为100000,去除了一个1。所有1去完时,n为0。

	public int NumberOf1(int n) {//正常算法
		int sum = 0;
		
		while(n != 0) {
			sum ++;
			n = n & (n-1);//&按位与
		}
		
		return sum;
    }
T12. 数值的整数次方
题目描述

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

解题思路

由于xn = (x*x)n/2,通过递归求解可减小时间复杂度,每递归一次,n 减一半。时间复杂度O(logn)。

注意:需要小心 n 的正负、奇偶。

	public double Power(double base, int exponent) {
		if(exponent == 0) return 1;
	    if(exponent == 1) return base;
	    
	    boolean isPositive = true;//正负次方以此判断
	    if(exponent < 0) {
	    	exponent = -exponent;
	    	isPositive = false;
	    }
	    
	    double pow = Power(base * base, exponent / 2);//递归,每次递归n减小一半,即 (x*x)^(n/2)
	    if(exponent % 2 != 0) pow *= base;//奇次幂的话,/2会少算一次,在这补回来
	    
	    return isPositive ? pow : (1 / pow);//三元表达式,正次幂为pow,负次幂为1/pow
    }
T13. 调整数组顺序使奇数位于偶数前面
题目描述

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

解题思路

先统计奇偶数的个数。在所要求的数组中,奇数的个数即为数组中偶数应该开始储存的位置。

clone一个数组,按顺序往原数组里存,奇数存前面,偶数存后面。

	public void reOrderArray(int[] array) {
	    int oddNum = 0;
	    for(int value: array) {
	    	if(value % 2 == 1) {
	    		oddNum++;
	    	}
	    }
	    
	    int[] copyArray = array.clone();//克隆数组,对原数组赋值
	    int i = 0, j = oddNum;//j为偶数开始存储的位置
	    
	    for(int num : copyArray) {
	        if(num % 2 == 1) {
	        	array[i] = num;
	        	i ++;
	        } else {
	        	array[j++] = num;
	        	j ++;
	        }
	    }
	}

或者,声明两个ArrayList存奇数和偶数,然后合并。

	public void reOrderArray(int[] array) {
        ArrayList<Integer> odd = new ArrayList<>();
        ArrayList<Integer> even = new ArrayList<>();
        
        for(int i = 0; i < array.length; i ++) {
        	if(array[i] % 2 == 0) {
        		even.add(array[i]);
        	} else {
        		odd.add(array[i]);
        	}
        }
        odd.addAll(even);

        for(int i = 0; i < array.length; i ++) {
        	array[i] = odd.get(i);
        }
    }
T14. 链表中倒数第 K 个结点
题目描述

输入一个链表,输出该链表中倒数第k个结点。

解题思路

假设,共 n 个结点,倒数第k个结点即为第 n-k+1 个结点。

定义两个指针node1和node2,使他们都指向第一个结点。到第 n-k+1 个结点则需要移动 n-k次。

此时,node1移动n次会指向空。

先让node1移动 k 次,还剩 n-k 次指向空。

这时,node2与node1同步移动,当node1指空时,node2移动了 n-k 次,刚好到第 n-k+1 个结点。

	public ListNode FindKthToTail(ListNode head, int k) {
		if(head == null) return null;
		
		ListNode node1 = head;
		ListNode node2 = head;
		
		while(node1 != null && k > 0) {//node1移动k次,还有n-k次会指空
			node1 = node1.next;
			k --;
		}
		
		if(k > 0) return null;
		
		while(node1 != null) {//移动n-k次,此时node2到n-k+1个结点,即倒数第k个结点
			node1 = node1.next;
			node2 = node2.next;
		}
		
		return node2;
	}
T15. 反转链表
题目描述

输入一个链表,反转链表后,输出新链表的表头。

解题思路

关键在于声明一个结点preNode用来记录前一个结点,使下一次循环时的结点的next指向它,反转顺序。

	public ListNode ReverseList(ListNode head) {
		if(head == null || head.next == null) return head;
		
		ListNode preNode = null;
		
		while(head != null) {
			ListNode tmp = head.next;//tmp记录【下一个结点】
			head.next = preNode;//【当前结点】的next指向【前一个结点】,反转链表顺序
			preNode = head;//preNode记录【当前结点】,即【下一个结点】的【前一个结点】
			head = tmp;//将【当前结点】更改为【下一个结点】,进入下一次循环
		}
		//当head为null时跳出了循环,此时它的前一个结点preNode即为原链表的最后一个结点,链表顺序已反转
		return preNode;
    }
T16. 合并两个排序的链表
题目描述

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

解题思路

声明一个新链表,不断比较原来两个链表的 val 值,小的插入新链表即可。

注意:要求返回的表头是声明的链表的next结点。

	public ListNode Merge(ListNode list1, ListNode list2) {
		ListNode head = new ListNode(-9999);
		ListNode tmp = head;
		
		while(list1 != null && list2 != null) {
			if(list1.val < list2.val) {//比较两个链表当前结点的值,小的先插入新链表
				tmp.next = list1;
				list1 = list1.next;
			} else {
				tmp.next = list2;
				list2 = list2.next;
			}
			tmp = tmp.next;
		}
		
		//容错,将剩余链表补到新链表结尾,此时能保持单调不减
		if(list1 != null) tmp.next = list1;
		if(list2 != null) tmp.next = list2;
		
		return head.next;//记得head为声明的无意义表头,head.next才是新链表的头
    }

项目地址https://github.com/JohnnyJYWu/offer-Java

下一篇算法 | 一周刷完《剑指Offer》 Day2:第17~26题

希望这篇文章对你有帮助~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值