剑指Offer试题总结
2013-11-27 18:47
274 查看
此篇文章是记录《剑指Offer》书中觉得有启发性的算法题和相关考题,以后面试前再拿来扫一遍,记录贴。
-------------------------------------------------我是邪恶的昏割线------------------------------------------------------
二维数组查找问题:一个二维数组的每一行是递增序列,每一列也是递增序列,要求判断一个数A是否在该数组中?
为了缩小比较范围,希望比较的时候能舍弃尽量多的无用数据,如果从右上角数B开始比较,若A大于B,则B所处的行被淘汰,如果A小于B,则B所处的列被淘汰。
将数组中的空格替换为%20,假设字符串后内存足够?
如果正序匹配空格并替换的话,其后的字符每次都要后移,时间复杂度为O(n^2)。Another Way,首先遍历数组,统计出空格数量N,这样替换后的数组大小比之前多出了2N。然后从数组末尾开始,如果当前字符不是空格,则一直往后移,如果是空格,则替换即可,这样时间复杂度为O(n)。
有两个有序数组A1和A2,A1后内存足够多,求将A2加入到A1中,结果仍为有序?
从A1的末尾,A2的末尾,开始比较,假设A2当前比较的数a2,A1当前比较的数a1,如果a1大于a2,则a1一直往后移,否则先放入a2。这样,A1中的每个数只移动了一次。
从尾到头打印链表?
这个用栈做个中间存储就行了
重建二叉树:给定前序遍历序列和中序遍历序列,构造出此二叉树?
前序遍历确定根节点,然后查找中序序列中该根节点的位置,从而分离出左子树和右子树,然后就是递归了。
代码如下:
旋转数组的最小数字:把一个数组的最开始的若干元素搬到数组的末尾,称之为数组的旋转。输入一个递增数组的旋转,要求输出最小元素。如{3,4,5,1,2}是{1,2,3,4,5,}的一个旋转,则输出1?
遍历数组的时间复杂度为O(n),Another way,采用二分查找,但是要注意元素相等的情况。
代码如下:
*递归与循环:递归代码简洁,但大多数情况下,不如循环容易理解,效率普遍不如循环,而且深层递归会导致内存栈溢出等情况。
一只青蛙一次可以跳上一级台阶,也可以跳上2级台阶,求该青蛙跳上一个n级台阶有多少种跳法?
其实上是一个斐波拉契数列。设跳上n级台阶对应的跳法数量为函数f(n),如果第一次跳上1级台阶,则后n-1级台阶的跳法数量为f(n-1);如果第一次跳上2级台阶,则后n-2级台阶的跳法数量为f(n-2)。所以,f(n) = f(n-2) + f(n-1),即斐波拉契数列。
青蛙跳台变种:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶,······,也可以跳上n级台阶,求该青蛙跳上一个n级台阶有多少种跳法?
同上f(n),如果第一次跳1级台阶,则后面有f(n-1)种跳法,如果第一次跳2级台阶,则后面有f(n-2)种跳法,······,如果第一次跳n级台阶,则后面有f(0)种跳法,即f(n) = f(0)+f(1)+...+f(n-1),而f(n-1)=f(fn-2)+...+f(0),带入有f(n)=2*f(n-1)。over。
在Excel中,用A表示第1列,B表示第2列,···,Z表示第26列,AA表示第27列,AB表示第28列,以此类推。请写出一个函数,输入用字母表示的列号编码,输出它是第几列?
其实就是把26进制的数转换成10进制
求二进制中1的个数:输入一个十进制整数,输出该数二进制表示中1的个数?
方法1:将该整数先与1做与运算,因为1除了最后一位是1,其余位都是0,所以结果如果为1,表示整数的最后一位为1,如果结果为0,表示整数的最后一位为0.然后将1左移一位,此时只有倒数第二位为1,其余都为0,此时与整数做交运算,则可以知道整数的倒数第二位是否为1,依次类推。
代码如下:
方法2:整数减一后,从二进制上解释,即是整数最后一个1变成0,后面的全变成1,而前面的不变。此时将原整数与该整数做与运算,则会将整数最后一个1变成0,其余位都不变。这样,只要整数不为0,迭代几次,就表示有几个1.即:N&(N-1)的结果是将N的最后一个1变为0.
代码如下:
判断一个整数是不是2的整数次方2?
因为2的整数次方的二进制只有1位为1,其他均为0,所以可以按照上面方法2的思想,将这个整数减一再做与运算,如果结果为0,则是2的整数次方。
输入两个整数m和n,计算需要改变m的二进制中的几位,才能与n相等?
既然要改变,说明之前不相等,所以可以将m与n做异或运算,这样结果中1的个数即是m需要改变的位数,然后按照上面的思想,计算1的个数即可。
*double和float等小数,在做比较的时候,不能完全用==比较,因为有存储的误差存在,需要用差值是否小于0.00...1这样比较。
实现乘方函数Power(double base, int exp),不用考虑大数问题?
首先,exp是否为0,是正是负,这些边界条件需要考虑周全。其次,在效率上,如果指数为偶数,乘方可以转换成其1半的平方的思想。这样可以减少乘法的次数。
打印1到最大的n位数,打印1到最大的3位数,即1到999?
考虑到大数问题,除了可以用字符串模拟加法外,因为结果是每位上0-9的排列,所以用递归更简单。
代码如下:
给定单链表的头指针和一个结点指针,需要在O(1)时间内删除则个结点指针?
如果遍历的话,复杂度为O(n);可以将该结点的下一个结点,假设为next,复制到该结点,然后再将该结点指向Next的next,最后删除next即可。需要考虑到边界情况,如结点指针为尾结点,单链表节点数为1等情况。
调整数组顺序,使奇数位于偶数前面?
维护两个指针p1,p2,分别指向数组的头和尾,p1只向后移动,p2只向前移动,一旦p1指向的值为偶数,p2指向的值为奇数,则调换两个值,直到p1和p2相遇。当然,这里如果想要增强代码的可扩展性,可以将判断是否该交换值的那个条件抽象成一个函数。
链表中倒数第K个结点:输出单链表中倒数第K个结点?
维护两个指针,第一个指针先从头指针向后移动K-1个结点,然后两个指针同时向后移动,当第一个指针到末尾的时候,此时第二个指针指向的结点即为倒数第K个结点,同时注意代码的鲁棒性,考虑参数的边界情况,代码如下:
反转链表:输入一个单链表的头结点,反转该链表并输出反转后链表的头结点?
这个主要注意代码的鲁棒性即可,没有什么技巧性,非递归和递归的代码如下:
合并有序链表:输入两个递增排序的列表,合并这两个链表使新链表中的结点仍是递增排序的?
这个没啥技巧性,注意鲁棒性,代码如下:
这个也没啥技巧性,从A中找到和B根节点相等的结点,然后依次判断左右结点是否相同,直到叶节点,
代码如下:
-------------------------------------------------我是邪恶的昏割线------------------------------------------------------
二维数组查找问题:一个二维数组的每一行是递增序列,每一列也是递增序列,要求判断一个数A是否在该数组中?
为了缩小比较范围,希望比较的时候能舍弃尽量多的无用数据,如果从右上角数B开始比较,若A大于B,则B所处的行被淘汰,如果A小于B,则B所处的列被淘汰。
将数组中的空格替换为%20,假设字符串后内存足够?
如果正序匹配空格并替换的话,其后的字符每次都要后移,时间复杂度为O(n^2)。Another Way,首先遍历数组,统计出空格数量N,这样替换后的数组大小比之前多出了2N。然后从数组末尾开始,如果当前字符不是空格,则一直往后移,如果是空格,则替换即可,这样时间复杂度为O(n)。
有两个有序数组A1和A2,A1后内存足够多,求将A2加入到A1中,结果仍为有序?
从A1的末尾,A2的末尾,开始比较,假设A2当前比较的数a2,A1当前比较的数a1,如果a1大于a2,则a1一直往后移,否则先放入a2。这样,A1中的每个数只移动了一次。
从尾到头打印链表?
这个用栈做个中间存储就行了
重建二叉树:给定前序遍历序列和中序遍历序列,构造出此二叉树?
前序遍历确定根节点,然后查找中序序列中该根节点的位置,从而分离出左子树和右子树,然后就是递归了。
代码如下:
/** * 给定二叉树的前序遍历和中序遍历,重构出此二叉树 * * @author * */ class Node{ int value; Node leftChild; Node rightChild; @Override public String toString() { // TODO Auto-generated method stub return "value:" + value +" leftchild value:" + (leftChild == null ? "null" : leftChild.value) +"rightchild value:" + (rightChild == null ? "null" : rightChild.value); } } public class RebuildBinaryTree { /** * 给定前序遍历序列和中序遍历序列,和各自的搜索范围,确定当前(子)树的根节点 * * @param preList 前序遍历序列 * @param start1 前序序列开始下标 * @param end1 前序序列结束下标 * @param midList 中序遍历序列 * @param start2 中序序列开始下标 * @param end2 中序序列结束下标 * * @return 根节点 * @throws Exception */ private static Node getRoot(int[] preList, int start1, int end1, int[] midList, int start2, int end2) { //参数合法性检测 if(start1 > end1 || start2 > end2 || preList.length < end1 || midList.length < end2){ return null; } //根节点为前序序列中搜索范围内的第一个节点 Node root = new Node(); root.value = preList[start1]; //将根节点带入中序序列中,从而分割出左子树和右子树,返回分割点的位置下标 int divideIndex = getIndex(midList, root.value, start2, end2); //计算左子树长度 int offset = divideIndex - start2 -1; //分别获取左右子树的根节点 root.leftChild = getRoot(preList, start1+1, start1 + offset + 1, midList, start2, start2 + offset); root.rightChild = getRoot(preList, start1 + offset + 2, end1, midList, divideIndex + 1, end2); System.out.println(root); return root; } /** * 从指定数组中找出目标的位置 * * @param array 指定数组 * @param des 查找对象 * @param start 开始下标 * @param end 结束下标 * @return */ private static int getIndex(int[] array, int des, int start, int end){ int len = array.length; //参数合法性检测 if(start > end || len < end) return -1; for(int i = 0; i < len; i++){ if(des == array[i]) return i; } return -1; } public static void main(String[] agrs){ int[] preList = {1,2,4,7,3,5,6,8,}; int[] midList = {4,7,2,1,5,3,8,6,}; Node root = getRoot(preList, 0, preList.length-1, midList, 0, midList.length-1); } }
旋转数组的最小数字:把一个数组的最开始的若干元素搬到数组的末尾,称之为数组的旋转。输入一个递增数组的旋转,要求输出最小元素。如{3,4,5,1,2}是{1,2,3,4,5,}的一个旋转,则输出1?
遍历数组的时间复杂度为O(n),Another way,采用二分查找,但是要注意元素相等的情况。
代码如下:
/** * 旋转数组:把数组最开始的若干个元素搬到数组的末尾。 * 现输入一个递增数组的旋转,要求输出最小元素。 * 如输入{3,4,5,1,2},输出1 * * @author * */ public class RotateArray { /** * 返回旋转数组的最小元素 * * @param numbers * @return * @throws Exception */ public static int getMin(int[] numbers) throws Exception{ if(numbers == null) throw new Exception("ERROR: Array cannot be null."); if(numbers.length == 0) throw new Exception("ERROR: Array length cannot be 0."); //定义起止下标 int start = 0; int end = numbers.length - 1; //定义二分法的中间下标,初始化为起始下标 int mid = start; while(numbers[start] >= numbers[end]){ if(end - start == 1){ mid = end; break; } mid = (start + end) / 2; //如果起始、终止和中间下标对应的值都相等,则顺序查找 if(numbers[start] == numbers[mid] && numbers[mid] == numbers[end]){ int result = numbers[start]; for(int i = start+1; i < end; i++){ if(numbers[i] < result) result = numbers[i]; } return result; } //缩短查找范围 if(numbers[start] <= numbers[mid]) start = mid; else if(numbers[end] >= numbers[mid]) end = mid; } return numbers[mid]; } public static void main(String[] agrs) throws Exception{ int[] numbers = {3,4,5,6,7,8,9,0,1}; System.out.println(getMin(numbers)); } }
*递归与循环:递归代码简洁,但大多数情况下,不如循环容易理解,效率普遍不如循环,而且深层递归会导致内存栈溢出等情况。
一只青蛙一次可以跳上一级台阶,也可以跳上2级台阶,求该青蛙跳上一个n级台阶有多少种跳法?
其实上是一个斐波拉契数列。设跳上n级台阶对应的跳法数量为函数f(n),如果第一次跳上1级台阶,则后n-1级台阶的跳法数量为f(n-1);如果第一次跳上2级台阶,则后n-2级台阶的跳法数量为f(n-2)。所以,f(n) = f(n-2) + f(n-1),即斐波拉契数列。
青蛙跳台变种:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶,······,也可以跳上n级台阶,求该青蛙跳上一个n级台阶有多少种跳法?
同上f(n),如果第一次跳1级台阶,则后面有f(n-1)种跳法,如果第一次跳2级台阶,则后面有f(n-2)种跳法,······,如果第一次跳n级台阶,则后面有f(0)种跳法,即f(n) = f(0)+f(1)+...+f(n-1),而f(n-1)=f(fn-2)+...+f(0),带入有f(n)=2*f(n-1)。over。
在Excel中,用A表示第1列,B表示第2列,···,Z表示第26列,AA表示第27列,AB表示第28列,以此类推。请写出一个函数,输入用字母表示的列号编码,输出它是第几列?
其实就是把26进制的数转换成10进制
求二进制中1的个数:输入一个十进制整数,输出该数二进制表示中1的个数?
方法1:将该整数先与1做与运算,因为1除了最后一位是1,其余位都是0,所以结果如果为1,表示整数的最后一位为1,如果结果为0,表示整数的最后一位为0.然后将1左移一位,此时只有倒数第二位为1,其余都为0,此时与整数做交运算,则可以知道整数的倒数第二位是否为1,依次类推。
代码如下:
public static int method1(int number){ int count = 0; int flag = 1; while(flag > 0){ if((number&flag) != 0){ count++; } flag = flag<<1; } return count; }
方法2:整数减一后,从二进制上解释,即是整数最后一个1变成0,后面的全变成1,而前面的不变。此时将原整数与该整数做与运算,则会将整数最后一个1变成0,其余位都不变。这样,只要整数不为0,迭代几次,就表示有几个1.即:N&(N-1)的结果是将N的最后一个1变为0.
代码如下:
public static int method2(int number){ int count = 0; while(number > 0){ count++; number = number & (number - 1); } return count; }
判断一个整数是不是2的整数次方2?
因为2的整数次方的二进制只有1位为1,其他均为0,所以可以按照上面方法2的思想,将这个整数减一再做与运算,如果结果为0,则是2的整数次方。
输入两个整数m和n,计算需要改变m的二进制中的几位,才能与n相等?
既然要改变,说明之前不相等,所以可以将m与n做异或运算,这样结果中1的个数即是m需要改变的位数,然后按照上面的思想,计算1的个数即可。
*double和float等小数,在做比较的时候,不能完全用==比较,因为有存储的误差存在,需要用差值是否小于0.00...1这样比较。
实现乘方函数Power(double base, int exp),不用考虑大数问题?
首先,exp是否为0,是正是负,这些边界条件需要考虑周全。其次,在效率上,如果指数为偶数,乘方可以转换成其1半的平方的思想。这样可以减少乘法的次数。
打印1到最大的n位数,打印1到最大的3位数,即1到999?
考虑到大数问题,除了可以用字符串模拟加法外,因为结果是每位上0-9的排列,所以用递归更简单。
代码如下:
/** * 打印1到最大的n位数,如打印1到最大的3位数: * 即打印从1到999 * * @author * */ public class Print1ToMax { /** * 入口函数 * * @param n 最大的n位数 */ public static void print1ToMax(int n){ if(n <= 0) return; char[] number = new char ; for(int i = 0; i < 10; i++){ number[0] = (char) (i + '0'); print1ToMaxRecursively(number, n, 0); } } /** * 迭代函数,如果已到最高位了,则打印;否则继续 * * @param number * @param length * @param index */ public static void print1ToMaxRecursively(char[] number, int length, int index){ //此时已经设置到最高位了,打印 if(index == length - 1){ printNumber(number); return; } for(int i = 0; i < 10; i++){ number[index + 1] = (char) (i + '0'); print1ToMaxRecursively(number, length, index + 1); } } /** * 从第一个非零数字开始打印 * * @param number */ public static void printNumber(char[] number){ boolean flag = true; for(char num : number){ if(flag && num != '0'){ flag = false; } if(!flag) System.out.print(num); } System.out.println(); } public static void main(String[] args) throws Exception{ print1ToMax(1); } }
给定单链表的头指针和一个结点指针,需要在O(1)时间内删除则个结点指针?
如果遍历的话,复杂度为O(n);可以将该结点的下一个结点,假设为next,复制到该结点,然后再将该结点指向Next的next,最后删除next即可。需要考虑到边界情况,如结点指针为尾结点,单链表节点数为1等情况。
调整数组顺序,使奇数位于偶数前面?
维护两个指针p1,p2,分别指向数组的头和尾,p1只向后移动,p2只向前移动,一旦p1指向的值为偶数,p2指向的值为奇数,则调换两个值,直到p1和p2相遇。当然,这里如果想要增强代码的可扩展性,可以将判断是否该交换值的那个条件抽象成一个函数。
链表中倒数第K个结点:输出单链表中倒数第K个结点?
维护两个指针,第一个指针先从头指针向后移动K-1个结点,然后两个指针同时向后移动,当第一个指针到末尾的时候,此时第二个指针指向的结点即为倒数第K个结点,同时注意代码的鲁棒性,考虑参数的边界情况,代码如下:
/** * 输出单链表中倒数第K个结点,注意边界情况 * * @author * */ class Node{ int value; Node next; public Node(int value){ this.value = value; } } public class FindKthToTail { public static Node getKthNode(Node head, int k) throws Exception{ //边界情况,如果链表为空或倒数位数小于1时,直接返回NULL if(head == null || k < 1) return null; Node first = head; Node second = head; for(int i = 0; i < k-1; i++){ if(first.next != null){ first = first.next; }else { return null; } } while(first.next != null){ first = first.next; second =second.next; } return second; } }
反转链表:输入一个单链表的头结点,反转该链表并输出反转后链表的头结点?
这个主要注意代码的鲁棒性即可,没有什么技巧性,非递归和递归的代码如下:
/** * 反转链表(递归) * * @param nowNode 当前处理的结点,初始为头结点 * @param pre 当前处理结点的前一结点,初始为null * * @return 反转链表的头结点 */ public static Node reverseRecursive(Node nowNode, Node preNode){ if(nowNode == null) return preNode; Node next = nowNode.next; //下一节点为前结点 nowNode.next = preNode; return reverseRecursive(next, nowNode); } /** * 反转链表(非递归) * * @param head 初始链表的头结点 * @return * @throws Exception */ public static Node reverse(Node head){ //如果链表为空会链表为单节点,则直接返回即可 if(head == null || head.next == null) return head; //标记反转过程中的新的头结点 Node newHead = null; //当前反转的结点 Node nowNode = head; //当前结点的前一个结点 Node preNode = null; //如果当前节点不为空,则让下一节点作为新的头结点,并更新当前结点等 while(nowNode != null){ Node next = nowNode.next; if(next != null) newHead = next; nowNode.next = preNode; preNode = nowNode; nowNode = next; } return newHead; }
合并有序链表:输入两个递增排序的列表,合并这两个链表使新链表中的结点仍是递增排序的?
这个没啥技巧性,注意鲁棒性,代码如下:
/** * 合并两个排好序的单链表,新链表仍为有序,<升序> * * @param head1 * @param head2 * @return */ public static Node merge(Node head1, Node head2){ if(head1 == null) return head2; if(head2 == null) return head1; Node newHead = null; //每次选择较小的头结点加入新链表即可 if(head1.value < head2.value){ newHead = head1; newHead.next = merge(head1.next, head2); }else{ newHead = head2; newHead.next = merge(head1, head2.next); } return newHead; }树的子结构:输入两颗二叉树A和B,判断B是不是A的子结构?
这个也没啥技巧性,从A中找到和B根节点相等的结点,然后依次判断左右结点是否相同,直到叶节点,
代码如下:
/** * 判断二叉树B是不是二叉树A的子树 * * @author * */ class BinaryNode{ int value; BinaryNode leftNode; BinaryNode rightNode; public BinaryNode(int value){ this.value = value; } } public class SubTree { private static boolean Tree1HasTree2(BinaryNode head1, BinaryNode head2){ //先判断子树,否则当两者都为Null的时候,实际是子树的,可是却返回false了 if(head2 == null) return true; if(head1 == null) return false; if(head1.value != head2.value) return false; return (Tree1HasTree2(head1.leftNode, head2.leftNode) && Tree1HasTree2(head1.rightNode, head2.rightNode)); } public static boolean isSubTree(BinaryNode head1, BinaryNode head2){ boolean result = false; if(head1 != null && head2 != null){ if(head1.value == head2.value) result = Tree1HasTree2(head1, head2); //继续找相同的结点 if(!result) result = isSubTree(head1.leftNode, head2); if(!result) result = isSubTree(head1.rightNode, head2); } return result; }二叉树的镜像:非叶结点的左右子结点互换?
/** * 树的镜像:非叶结点的左右子节点互换(递归) * * @param head */ public static void mirrorTreeRecursive(BinaryNode head){ //如果二叉树为空或只有一个结点,则不用变换了 if(head == null) return; if(head.leftNode == null && head.rightNode == null) return; //交换左右子节点 BinaryNode tmp = head.leftNode; head.leftNode = head.rightNode; head.rightNode = tmp; if(head.leftNode != null) mirrorTree(head.leftNode); if(head.rightNode != null) mirrorTree(head.rightNode); }
相关文章推荐
- 剑指offer-第十题方法总结
- 剑指offer总结——1
- 剑指Offer——乐视笔试题+知识点总结
- 剑指offer——斐波那契数列相关问题总结
- 牛客网做题总结:剑指offer中题目,java版一
- 剑指Offer学习总结-二进制中1的个数
- 剑指Offer学习总结-数值的整数次方
- 剑指Offer学习总结-把数组排成最小的数
- 剑指offer:排序算法实现经验教训总结
- 剑指offer第四题方法总结
- 剑指Offer学习总结-数组中出现次数超过一半的数字
- 剑指Offer学习总结-第一个只出现一次的字符
- 剑指Offer——顺丰笔试题+知识点总结
- 牛客网做题总结:剑指offer中题目,java版二
- 剑指Offer学习总结-用两个栈实现队列
- 剑指offer-第三题方法总结
- 剑指Offer-第一章面试细节总结
- 剑指Offer学习总结-栈的压入、 弹出序列
- 剑指Offer——咪咕笔试题+知识点总结
- 剑指offer-第三十五题方法总结