剑指offer(java)

3.数组中重复的数字

3.1 可修改数组

题目
在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复的数字2或者3。
思路可用哈希表,但这样时间复杂度是O(n),空间复杂度也是O(n)
这里用的交换的思想,如果这个数组中没有重复的数字,那么当数组排序之后数字i应该出现在下标为i的位置。

public class Solution2 {
	/**
     * 找到数组中一个重复的数字
     * 返回-1代表无重复数字或者输入无效
     */
    public int getDuplicate(int[] arr) {
        if (arr == null || arr.length <= 0) {
            System.out.println("数组输入无效!");
            return -1;
        }
        for (int a : arr) {
            if (a < 0 || a > arr.length - 1) {
                System.out.println("数字大小超出范围!");
                return -1;
            }
        }
        for (int i = 0; i < arr.length; i++) {
            int temp;
            while (arr[i] != i) {
                if (arr[arr[i]] == arr[i])
                    return arr[i];
                // 交换arr[arr[i]]和arr[i]
                temp = arr[i];
                arr[i] = arr[temp];
                arr[temp] = temp;
            }
        }
        System.out.println("数组中无重复数字!");
        return -1;
    }
}

时间复杂度:O(n),虽有一个两重循环,但每个数字只要一次交换就能被确定位置,n个数字,最多只要n次。
空间复杂度:O(1),所有步骤都在输入的数组上进行,不需要额外分配内存(交换的思想),、

3.2 不可修改数组

题目

思路 数组长度为n+1,而数字只从1到n,说明必定有重复数字。可以由二分查找法拓展:把1-n的数字从中间数字m分成两部分,若前一半1-m的数字数目超过m个,说明重复数字在前一半区间,否则,在后半区间m+1~n。每次在区间中都一分为二,知道找到重复数字。

public class Solution2 {
	public static int getDuplicate(int[] arr) {
		if (arr == null || arr.length <= 0) {
			System.out.println("数组输入无效!");
			return -1;
		}
		for (int a : arr) {
			if (a < 1 || a > arr.length - 1) {
				System.out.println("数字大小超出范围!");
				return -1;
			}
		}
		int low = 1;
		int high = arr.length - 1; // high即为题目的n
		int mid, count;
		while (low <= high) {
			mid = ((high - low) >> 1) + low;
			count = countRange(arr, low, mid);
			if (low == high) {
				if (count > 1) //如果重复,次数必定大于1
					return low;
				else
					break; // 意味着count=1,不过必有重复,应该不会出现这种情况吧?
			}
			if (count > mid - low + 1) {
				high = mid; //这边为什么不是high=mid+1,因为如2,3,5,4,3,2,6,7
				//1-4范围的数字出现了5次,那么1-4范围内必定有重复,那么范围就缩减到了1-4,而不是1-5(mid=4)
			} else {
				low = mid + 1;//同理,1-4若出现了4次,那5-7必定出现了4次,那么范围就缩减到了5-7
			}
		}
		return -1;
	}

	/**
	 * 返回在[low,high]范围中数字的个数
	 */
	public static int countRange(int[] arr, int low, int high) {
		if (arr == null)
			return 0;

		int count = 0;
		for (int a : arr) {
			if (a >= low && a <= high)
				count++;
		}
		return count;
	}

	public static void main(String[] args) {
		System.out.print("test3:");
		int[] a = { 2, 3, 5, 4, 3, 2, 6, 7 };
		int dup = getDuplicate(a);
		if (dup >= 0)
			System.out.println("重复数字为:" + dup);
	}
}

时间复杂度:函数countRange()将被调用O(logn)次,每次需要O(n)的时间,因此时间复杂度:O(nlogn) (while循环为O(logn),coutRange()函数为O(n))
空间复杂度:O(1)

4.二维数组中的查找

题目
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
思路
查找整数时,如果从左上角开始查找,情况较为复杂,可以转换思路,从右上角开始查找:左边数字比较小,右边数字比较大,容易进行判断。

public class Solution2 {
	public boolean Find(int target, int[][] matrix) {
	    if (matrix == null || matrix.length == 0 || matrix[0].length == 0)//粗略的检查了行不为0,列不为0的情况
	        return false;
	    int rows = matrix.length, cols = matrix[0].length;//row:行    cols:列
	    int r = 0, c = cols - 1; // 从右上角开始
	    while (r <= rows - 1 && c >= 0) { //不知道什么时候停下来的情况使用while,而不是for
	        if (target == matrix[r][c])
	            return true;
	        else if (target > matrix[r][c])
	            r++;
	        else
	            c--;
	    }
	    return false;
	}
}

5.替换空格

题目
将一个字符串中的空格替换成 “%20”。
在这里插入图片描述
思路
首先要询问面试官是新建一个字符串还是在原有的字符串上修改,本题要求在原有字符串上进行修改。

若从前往后依次替换,在每次遇到空格字符时,都需要移动后面O(n)个字符,对于含有O(n)个空格字符的字符串而言,总的时间效率为O(n2)。

转变思路:先计算出需要的总长度,然后从后往前进行复制和替换,,则每个字符只需要复制一次即可。时间效率为O(n)。

public class Solution2 {
	public String replaceSpace(StringBuffer str) {
        if (str == null) {
            System.out.println("输入错误!");
            return null;
        }
        int length = str.length();
        int indexOfOriginal = length-1;
        //根据空格的数目重新定义字符串的长度
        for (int i = 0; i < str.length(); i++) {
            if (str.charAt(i) == ' ')
                length += 2;
        }
        str.setLength(length); //void setLength(int newLength)  
        //下面这种方法和上面的for循环一个目的
//        for (int i = 0; i <= P1; i++)
//            if (str.charAt(i) == ' ')
//                str.append("  ");
        int indexOfNew = length-1;
        while (indexOfNew > indexOfOriginal) { // 即只要还有空格就执行
        	char c = str.charAt(indexOfOriginal);
            if (c != ' ') {
                str.setCharAt(indexOfNew--, c);//void setCharAt(int index, char ch)  
            } else {
                str.setCharAt(indexOfNew--, '0');
                str.setCharAt(indexOfNew--, '2');
                str.setCharAt(indexOfNew--, '%');
            }
            indexOfOriginal--;
        }
        return str.toString();
    }
}

6.从尾到头打印链表

题目
输入一个链表的头结点,从尾到头反过来打印出每个结点的值。
思路
结点遍历顺序只能从头到尾,但是输出的顺序却为从尾到头,是典型的“后进先出”问题,这就要联想到使用栈,从而也可以联想到使用递归。

import java.util.Arrays;
import java.util.Stack;

public class Solution2 {
	private class ListNode {
		int key;
		ListNode next;

		public ListNode(int key) {
			this.key = key;
			this.next = null;
		}
	}

	// 采用栈
	public void printListReversingly_Iteratively(ListNode node) {
		Stack<ListNode> stack = new Stack<ListNode>();
		while (node != null) { //存入栈
			stack.push(node);
			node = node.next;
		}
		while (!stack.empty()) { //取出
			System.out.println(stack.pop().key);
		}
	}
}
	//使用递归
	public void printListReversingly_Recursively(ListNode node) {
		if (node != null) {
			printListReversingly_Recursively(node.next);
			System.out.println(node.key);
		}
		else { //这边的return可加可不加
			return;
		}
	}

7.重建二叉树

题目
根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
在这里插入图片描述
思路
递归,可见《剑指Offer》P63
遇到有关树的遍历,递归等,不用深推纠结,就拿初始两个步骤验证即可。

package easy_10;

import java.util.HashMap;
import java.util.Map;

public class Solution2 {
	private class TreeNode {
		int val;
		TreeNode left;
		TreeNode right;

		public TreeNode(int val) {
			this.val= val;
			this.left = null;
			this.right = null;
		}
	}

	private Map<Integer, Integer> indexForInOrders = new HashMap<>();
	 
	public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
	    for (int i = 0; i < in.length; i++)
	        indexForInOrders.put(in[i], i);
	    return reConstructBinaryTree(pre, 0, pre.length - 1, 0);
	}
	 
	private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL) {
	    if (preL > preR) //如preL=2,leftTreeSize=0;此时preL+1=3,preL + leftTreeSize=2
	        return null;
	    TreeNode root = new TreeNode(pre[preL]);//在前序数组中求根节点
	    int inIndex = indexForInOrders.get(root.val); //求根节点在中序数组中的位置
	    int leftTreeSize = inIndex - inL; //求出中序数组根节点为root时的左子树长度
	    //根据这个长度,在前序数组中将此左子树当作一个新的树求解(起点为pre+1,末尾点为preL+leftTreeSize)
	    //inL表示该数组在中序数组中的最左端索引
	    root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL);
	    root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1);
	    return root;
	}
	public static void main(String[] args) {
		int[] pre= {1,2,4,7,3,5,6,8};
		int[] in= {4,7,2,1,5,3,8,6};
		TreeNode node=new Solution2().reConstructBinaryTree(pre,in);
	}
}

8.二叉树的下一个节点

题目
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
思路
首先自己在草稿纸上画图,进行分析(不再展开)。可以发现下一个结点的规律为:

  • 若当前结点有右子树时,其下一个结点为右子树中最左子结点;
  • 若当前结点无右子树时,
    1. 若当前结点为其父结点的左子结点时,其下一个结点为其父结点;
    2. 若当前结点为其父结点的右子结点时,继续向上遍历父结点的父结点,直到找到一个结点 是父结点的左子结点(与1.中判断相同),该结点即为下一结点。(好好意会)
import java.util.HashMap;
import java.util.Map;

public class Solution2 {
	private class TreeLinkNode {
		int val;
		TreeLinkNode left = null;
		TreeLinkNode right = null;
		TreeLinkNode parent = null;

		TreeLinkNode(int val) {
			this.val = val;
		}
	}

	public TreeLinkNode GetNext(TreeLinkNode pNode) {
		if (pNode == null) {
			System.out.print("结点为null ");
			return null;
		}
		//如果一个节点有右子树,那么它的下一个节点就是它的右子树中的最左子节点
		if (pNode.right != null) {
			pNode = pNode.right;
			while (pNode.left != null)
				pNode = pNode.left;
			return pNode;
		}
		//如果一个节点没有右子树的情况
		while (pNode.parent != null) {  //大前提:没右子树但有父节点的情况
			if (pNode == pNode.parent.left) { //如果该节点是他父节点的左子节点,那么它的下一个节点就是他的父节点
				return pNode.parent;   //该节点是他父节点的左子节点的情况(该节点是他父节点的右子节点的情况包含这种情况)
			}
			pNode = pNode.parent;  //如果该节点是他父节点的右子节点,就需要沿着父节点一直向上爬,直到
			//找到一个节点,该节点是其父节点的左子节点。
		}
		return null;  //没右子树且没父节点的情况
	}
}

9.用两个栈实现队列

题目
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在队列尾部插入结点和在队列头部删除结点的功能。
思路
这道题较简单,自己先试着模拟一下插入删除的过程(在草稿纸上动手画一下):插入肯定是往一个栈stack1中一直插入;删除时,直接出栈无法实现队列的先进先出规则,这时需要将元素从stack1出栈,压到另一个栈stack2中,然后再从stack2中出栈就OK了。需要稍微注意的是:当stack2中还有元素,stack1中的元素不能压进来;当stack2中没元素时,stack1中的所有元素都必须压入stack2中。否则顺序就会被打乱。

import java.util.Stack;

public class Solution2 {
	Stack<Integer> in = new Stack<Integer>();
	Stack<Integer> out = new Stack<Integer>();
	 
	public void appednTail(int node) {
	    in.push(node);
	}
	 
	public int deleteHead() throws Exception {
	    if (out.isEmpty())
	        while (!in.isEmpty())
	            out.push(in.pop()); //必须一次性把in栈中的元素全部压入out栈
	 
	    if (out.isEmpty())  //如果in栈中没有元素压入out栈,那out栈就为空,那就取不了元素了
	        throw new Exception("queue is empty");
	 
	    return out.pop(); //如果out不为空,那就直接从out栈中弹出元素
	}
}

9.2用两个队列实现栈

思路
一个队列加入元素和弹出元素时,需要把队列A中的其他元素放到另外一个队列B中,然后在原来的队列A中删除最后一个元素。两个队列始终保持只有一个队列是有数据的

import java.util.LinkedList;
import java.util.Queue;

public class Solution2<T> {
	// 记住队列是接口!!!
	 
		private Queue<T> queue1 = new LinkedList<>();
		
		private Queue<T> queue2 = new LinkedList<>();

		//压栈、元素要插入到非空的队列
		public boolean push(T t) {
			if (!queue1.isEmpty()) {
				return queue1.offer(t);
			} else {
				return queue2.offer(t);
			}
		}
		
		/**
		 * 弹出并删除元素 
		 */
		public T pop() {
			if (queue1.isEmpty() && queue2.isEmpty()) {
				throw new RuntimeException("queue is empty");
			}
			if (!queue1.isEmpty() && queue2.isEmpty()) {
				while (queue1.size() > 1) {  
					queue2.offer(queue1.poll());//把queue1中的元素放入queue2中
				}
				return queue1.poll(); //此时quuee中只剩下一个元素了,将他从队列中取出
			}
			if (queue1.isEmpty() && !queue2.isEmpty()) {
				while (queue2.size() > 1) {
					queue1.offer(queue2.poll());
				}
				return queue2.poll();
			}
			return null;
		}
}

10.斐波那契数列

题目
在这里插入图片描述
思路
如果直接写递归函数,由于会出现很多重复计算,效率非常底,不采用。例如,计算 f(4) 需要计算 f(3) 和 f(2),计算 f(3) 需要计算 f(2) 和 f(1),可以看到 f(2) 被重复计算了。
  要避免重复计算,采用从下往上计算,可以把计算过了的保存起来,下次要计算时就不必重复计算了:先由f(0)和f(1)计算f(2),再由f(1)和f(2)计算f(3)……以此类推就行了,计算第n个时,只要保存第n-1和第n-2项就可以了。
可读性较高的写法

public class Solution2 {
	public long Fib(long n) {
        if(n<0)
            throw new RuntimeException("下标错误,应从0开始!");
        if (n == 0)
            return 0;
        if (n == 1)
            return 1;
            
        long prePre = 0;
        long pre = 1;
        long result = 1;
        
        for (long i = 2; i <= n; i++) { 
            /*考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储
        	前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。*/
            result = prePre + pre; 
            prePre = pre;//将prePre指针指到他后面一格pre上
            pre = result;//将pre指针指到他后面一格result上
            //0 + 1 = 2; prePre=0,pre=1;
            //1 + 2 = 3; prePre=1,pre=2;
        }
        return result;
    }
}

时间复杂度:O(n),for循环
空间复杂度:O(1)

第二种解法
思路:递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。,因此这里参照动态规划的思路来求解。

public class Solution2 {

	public int Fibonacci(int n) {
	    if (n <= 1)
	        return n;
	    int[] fib = new int[n + 1];//因为要取到fib[n]这个值,所以长度得是n+1
	    fib[1] = 1;
	    for (int i = 2; i <= n; i++)
	        fib[i] = fib[i - 1] + fib[i - 2];//简单明了,将所有的值都存储在数组里了
	    return fib[n];
	}
}

时间复杂度:O(n),for循环
空间复杂度:O(n),创建n+1长度的数组

10.2青蛙跳台阶问题

题目
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
思路
如果只有1级台阶,那只有一种跳法,如果有2级台阶,那么有两种跳法:一次跳1级,连续跳两次;或者一次跳2级。
现在我们把n级台阶时的跳法看成n的函数,记为f(n)。当n>3时,一是第一次跳有2种选择,如果跳1级,那么此时跳法的数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);二是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2)因此,n级台阶的不同跳法的总数f(n)=f(n-1)+f(n-2)。这不就是斐波那契数列吗?!

public class Solution2 {

	public int Fibonacci02(int n) {
	    if (n < 1)
	        return -1; //代表出异常了
	    int[] fib = new int[n + 1];
	    fib[1] = 1; //1级台阶只有一种跳法
	    fib[2] = 2; //2级台阶有两种跳法
	    for (int i = 3; i <= n; i++)
	        fib[i] = fib[i - 1] + fib[i - 2];
	    return fib[n];
	}
}

10.3矩形覆盖

题目
我们可以用 2X1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2X1 的小矩形无重叠地覆盖一个 2Xn 的大矩形,总共有多少种方法?
在这里插入图片描述
思路
和上面一样,是斐波那契数列的应用,一模一样,n=1时,一种,n=2时,两种。。。。

11.旋转数组的最小数字

题目
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3, 4, 5, 1, 2}为{1, 2, 3, 4, 5}的一个旋转,该数组的最小值为1。
思路
见《剑指Offer》P83,
数组在一定程度上是排序的,很容易分析出:可以采用二分法来寻找最小数字。将旋转数组对半分可以得到一个包含最小元素的新旋转数组,以及一个非递减排序的数组。新的旋转数组的数组元素是原数组的一半,从而将问题规模减少了一半,这种折半性质的算法的时间复杂度为 O(logN)
但是这里面有一些陷阱
  1.递增排序数组的本身是自己的旋转,则最小数字是第一个数字
  2.中间数字与首尾数字大小相等,如{1,0,1,1,1,1}和{1,1,1,1,0,1},无法采用二分法,只能顺序查找。

public class Solution2 {

	public class MinNumberInRotatedArray {
		public int minNumberInRotateArray(int[] array) {
			if (array == null || array.length <= 0) // 空数组或null时返回0
				return 0;

			int low = 0;
			int high = array.length - 1;
			int mid = (low + high) / 2;

			// 对应陷阱1,本身就是升序数组,若非如此,左边数组的第一个元素必定大于右边数组的最后一个元素
			if (array[low] < array[high])
				return array[low]; // 升序数组本身也是一个旋转数组
				
			// 对应陷阱2,中间数字与首尾数字相等,1 0 1 1 1 或者 1 1 1 0 1这种情况
			// 此时二分法失效,只能使用顺序查找,而此时只可能有如0,1;1,2等两种数字
			if (array[mid] == array[high] && array[mid] == array[low]) {
				for (int i = 1; i <= high; i++) {
					if (array[i] < array[i - 1]) // 相邻比较,注意首中尾三个相等的元素必定两种元素中较大的
						return array[i]; // 把较小者返回出来,该元素就是题目要求的最小值了
				}
				return array[low]; // 数组元素都相同的情况
			}

			// 正常情况
			while (low < high) {
				//前后指针紧挨着,此时low指向第一个数组的末尾,high指向第二个数组的开头 
				if (high - low == 1)
					break; 
				mid = (low + high) / 2;
				if (array[mid] <= array[high])
					high = mid;
				if (array[mid] > array[low])
					low = mid;
			}
			
			return array[high]; 
		}
	}
}

第二种更加精炼版本,推荐用这种,思路一模一样

public class Solution2 {

	public class MinNumberInRotatedArray {
		public int minNumberInRotateArray(int[] nums) {
			if (nums == null || nums.length <= 0) // 空数组或null时返回0
				return 0;

			int low = 0, high = nums.length - 1;
			while (low < high) { //最终会使得low=high(即最小值位置)而跳出循环
				int m = low + (high - low) / 2;
				if (nums[low] == nums[m] && nums[m] == nums[high])
					return minNumber(nums, low, high);
				else if (nums[m] <= nums[high])
					high = m; //说不定m指向的就是最小元素,因此不能high=mid-1
				else   //说明nums[m]>nums[high],m指针肯定指向左子树组,且最小元素在后面
					low = m + 1; //这也就是为什么这边m+1,上面却没有-1
			}
			return nums[low];
		}

		private int minNumber(int[] nums, int l, int h) {
			for (int i = l; i < h; i++)
				if (nums[i] > nums[i + 1])
					return nums[i + 1];
			return nums[l];
		}
	}
}

注意这段代码的一些细节:

1.使用low=mid+1,而不是low=mid,最终会使得low=high(即最小值位置)而跳出循环;

2.使用high=mid,而不是high=mid-1,因为有可能mid就是最小值点,不能减1;

12.矩阵中的路径

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 、4下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合;、下载 4使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合;、 4下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值