《剑指Offer》第二章(一)题3-8
为春招实习做准备,记录一下《剑指Offer》里面的面试题
第二章
面试题3:数组之中的重复数字。
这个题吧,虽然不难,但是不知道为什么就是看了很久,可能很久没有做算法题了。最后面一句话说的挺好的,给你出题之后,要问清楚题目,以及要求,时间效率优先还是空间效率优先,虽然我一般都会选择时间效率优先,因为内存现在都比较大了。
题目很简单,一个长度为n的数组,数字都在0~n-1,找出其中任意一个重复的数字,注意是任意一个。
书中讲到了三个算法:
1.时间复杂度是O(n),空间复杂度也是O(n)
遍历数组,碰到一个数字,先在哈希表之中判断是否存在该数字,如果不存在,则将其放入到哈希表之中,如果存在,则找出一个重复的数字。
很明显,需要一个O(n)的哈希表,遍历数组O(n)复杂度
我感觉这种挺好的,好理解,也好实现,最多有O(n)的空间复杂度,但是面试肯定不可能问你这么简单的。
2.时间复杂度是O(n),空间复杂度O(1)
首先明白最重要的一点,如果这个数组是没有重复数字的,那么当数组有序时,数字m就一定在m位置出现。同样的遍历数组,假设现在遍历到下标为i的数字m,判断i==m 是否成立,成立,则该数字就在他本身的位置,不成立,则判断数组之中下标为i,m两个数字是否相等,相等,则找到一个重复数字,不相等,则将下标为i,m的数字进行交换,将数字m放到他本来的位置,之后继续判断新的i下标处的数字,直到相等为止,i++,进行下一轮判断。
说实话一开始不理解他为什么要交换,最后想了一下,交换是为了让每一个数字都在它应该在的位置,这样当我们审查第i个数字m时,如果第m个数字就是m,已经被交换到了他应该在的位置,这时候比较就可以得出一个重复的数字。
下面是代码,可以看到,一定不能忘了对输入的有效性进行判断,这是一种良好的编程习惯吧,书上是c++,python 方便一点
class Solution: def findDuplicate(self, nums) -> int: # 找出一个重复数字 if nums == None: # 输入检测 return False length = len(nums) for num in nums: # 是否有超过范围的数字 if num > length-1 or num < 0: return False for i in range(length): # 遍历数组 while nums[i] != i: # 这个数字是否在它应该在的位置 if nums[i] == nums[nums[i]]: # 判断是否有重复 return nums[i] # 没有重复则进行交换,nums[i] 和 nums[nums[i]] temp = nums[i] nums[i] = nums[temp] nums[temp] = temp return False
可以看到虽然有两个循环,但是每一个数字最多交换两次就可以到它自己的位置,所以总体的复杂度为O(n)。
3.不能修改原来数组
这种其实题目都和上面那一个不一样,要求不能修改原来的数组,并且长度是n+1,所有数字都在1~n之间。
先说一下一点。
由于数组长度是n+1,但是数字的范围都是在1~n之间,所以肯定是有重复数字的,和之前不同。
第一种方法:
创建一个n+1大小的数组,遍历原来的数组,将数字m复制到新数组的下标为m的位置,如果m位置已经有了数字,那么就说明这个数字是重复的,时间空间复杂度都是O(n),还是比较简单的一种方法。
第二种方法:
时间复杂度O(nlogn),空间复杂度O(1)
基本思想是利用二分法逐渐缩小重复数字的区间(注意这个区间是1~n,而不是原来的数组),最后找到重复数字。将数字1~n从中间分为两部分,1~m和m+1~n,然后判断数字1~m在数组之中出现的次数,如果大于m,那么说明1~m之间有重复数字,反之就是m+1~n,之后用相同的方法,逐渐缩小区间即可。但是这种方法不能找出全部的重复数字,因为当出现次数小于等于m时,无法判断是一个数字出现了两次,还是每个数字都出现了一次,如果是前一种情况,那么就丢失了重复数字。
代码如下:
class Solution: def findDuplicate(self, nums) -> int: # 找出一个重复数字 if nums == None: # 输入检测 return False length = len(nums) for num in nums: # 是否有超过范围的数字 if num > length-1: return False print("2222") start = 1 end = length-1 # 对数字1-n,所以是length-1 while end >= start: middle = int(((end-start)/2) + start) # 计算中点 count = self.countRange(nums, start, middle) # 统计数组之中start~middle数字在数组之中出现的次数 if end == start: # 判断是否查找结束 if count > 1: # 说明有重复数字 return start else: break if count > (middle-start+1): # 比较出现次数和前值区间数字个数,大于则说明前置区间有重复数字 end = middle else: start = middle + 1 return False def countRange(self, nums, start, end): # 统计某一个区间之内数字在数组之中出现的次数 if nums == None: return False count = 0 for i in nums: if i <= end and i >= start: count += 1 return count
面试题4:
这个题目很简单,判断一个数字是否在一个二维数组之中存在,这个二维数组从左向右递增,从上向下递增,书中的解法很巧妙,从一个具体的问题入手,将复杂的问题普遍化。
从右上角数字开始,比较该数字,如果该数字大于目标数字,则剔除最后一列,因为该数字之下的数字都大于该数字,肯定也大于目标数字了,同理该数字小于目标数字,则剔除第一行,当然如果相等了就结束。右上角和左下角都可以,但是左上角和右下角不行,因为无法通过判断来剔除一行或者一列。代码如下:
class Solution:
def find(self, nums, target, rows, columns) -> int:
if nums == None or rows < 0 or columns < 0 or target == None:
return False
row = 0
column = columns-1
while row < rows and columns >= 0:
if nums[row][column] == target:
return True
elif nums[row][column] > target: # 剔除最后一列
column -= 1
elif nums[row][column] < target: # 剔除最上面一行
row += 1
return False
没什么说得,右上角数字就是nums[row][column]
面试题5:
这个题目也很简单,给你一个字符串,将里面的空格进行替换,替换成 %20 ,一般特殊字符替换规则都是替换为%+ASCII的十六进制表示。
注意一个问题,替换之后的字符串会变长,所以进行替换时必须进行移动,不然会出现内存覆盖的现象,比如 空格 替换为 %20,一个字符变成了三个字符,如果直接插入,那就会将该空格之后的两个字符覆盖掉。
时间复杂度是O(n2):n的平方
遍历字符串,碰到空格,就将空格之后的字符整体向后移动2个位置,然后将%20以当前位置为基准,插入即可。由于遇到空格就需要移动字符串,遍历又需要O(n)的复杂度,所以时间复杂度是O(N2)
时间复杂度为O(N)的方法:
很巧妙,反正我现在是想不到的。基本思路是先分配所需要的内存,从后向前遍历,这样子就不需要每次遇到空格都移动字符串了。具体来说,先遍历字符串,计算空格数量,然后计算出替换之后的字符串长度。接着声明两个指针,一个指向当前字符串的尾部p1,另一个指向替换之后字符串的尾部p2,使用p1从后向前进行遍历,对于碰到的字符,分两种情况进行讨论:
1.碰到了非空格字符,将p1所指的字符复制到p2位置处,并将p1,p2都向前移动一个位置
2.碰到了空格,此时,以p2为基准,在p2之前(包括p2)插入%20,并将p2向前移动三个位置,p1向前移动一个位置。
这样循环直到p1,p2互相重合,说明已经遍历完了字符串。
属实巧妙,相当于先计算出来需要多长的字符串,然后按照逻辑直接对字符串进行填充,而且空间复杂度也是O(1),虽然直接申请一个字符串,然后无脑复制过去也可以,但是需要O(N)的空间复杂度,而且也显得笨笨的。
代码如下:
class Solution{ public void ReplaceBlank(StringBuilder stringBuilder, int length){ if(stringBuilder==null || length < 0){ return; } int originalLength = 0; //原字符串长度 int numberOfBlank = stringBuilder.count; //字符串之中空格的数量 int i = 0; while(stringBuilder.charAt(i) != '\0'){ originalLength++; //增加源字符串长度 if(stringBuilder.charAt(i) == ' '){ numberOfBlank++; } i++; } int newLength = originalLength + numberOfBlank * 2; //新字符串长度 if(newLength > length){ return; } int indexOfOriginal = originalLength; int indexOfNew = newLength; while(indexOfOriginal >= 0 && indexOfNew > indexOfOriginal) { if(stringBuilder.charAt(indexOfOriginal) == ' '){ //空格,进行%20填充 --时候就能够向前移动了 stringBuilder.replace(indexOfNew--, indexOfNew+1, "0"); stringBuilder.replace(indexOfNew--, indexOfNew+1, "2"); stringBuilder.replace(indexOfNew--, indexOfNew+1, "%"); }else{ //不是空格,正常填充字符 stringBuilder.replace(indexOfNew--, indexOfNew+1, stringBuilder.substring(indexOfOriginal, indexOfOriginal+1)); } --indexOfOriginal; //原字符串指针向前移动 } } }
java真操蛋,感觉的真的难用。
面试题6:从尾到头打印链表
这个题比较简单,纯粹就是复习回顾的,直接上代码;不过注意分析问题时候,这个题相当于是第一个遍历的节点,最后一个输出,即先进后出,典型的栈的特点,所以直接用栈来进行存储或者递归就可以了。
class ListNode: def __init__(self, val=None): self.val = val self.next = None def reverseOut(head: ListNode): if head.next == None: return stack = [] while head.next: stack.append(head.next.val) head = head.next while stack: print(stack.pop()) # 测试 head = ListNode() node = head for i in range(10): node.next = ListNode(i) node = node.next reverseOut(head)
递归方法很简单,不过层数会很深,2n次方级别的
def recursion(head: ListNode): if head.next: recursion(head.next) print(head.next.val)
面试题7:重建二叉树
思路很简单,但是代码挺复杂的这个,暂时略过
面试题8,二叉树的下一个节点
题目:给定二叉树之中的一个节点,然后找出在中序遍历的结果之中,该节点的下一个遍历的节点。二叉树中每一个节点有一个额外的指针指向其父节点
问题分析:中序遍历很简单,“”左中右,所以可以想到,在当前节点遍历之后,如果它有右子树,那么就会遍历他的右子树最左边的那个节点,如果没有右子树的话,就说明当前节点和它的左子树(有的话)都已经遍历完了,该向上遍历了,根据中序遍历的特点,我们需要找到一个左子节点,以该左子节点为根节点的树都已经遍历完了,该遍历该左子节点的父节点了,所以它的父节点就是下一个需要遍历的节点。
所以,分为三种情况:
1.当前节点有右子树,那么下一个遍历的就是右子树的最左节点
当前节点没有右子树,可以分为两种情况
1.当前节点是左子节点,下一个遍历的就是该节点的父节点
2.当前节点是右节点并且没有右子树,有没有左子树不影响。向上回溯,找到一个左子节点,该节点的父节点即为所求。
但是其实1情况是二情况的一种特殊情况,即该节点本身就是左子节点,不需要再向上进行回溯,所以写代码时候当做一种情况写就可以了。
代码如下:
class BTree: def __init__(self, val=None, left=None, right=None, parent=None): self.val = val self.left = left self.right = right self.parent = parent def getNext(pNode: BTree): if pNode == None: return pNext = None # 有右子树的情况 if pNode.right != None: temp = pNode.right while temp.left: # 右子树的最左边的节点 temp = temp.left pNext = temp else: current = pNode parent = pNode.parent # 向上回溯,找到一个节点为左子节点,循环条件:当前节点是右子节点且有父节点 while parent != None and current == parent.right: current = parent parent = parent.parent pNext = parent return pNext
测试起来比较麻烦,因为需要构建节点的父指针,得一个一个设置,简单写一个
a = BTree(val=0) d = BTree(val=3) g = BTree(val=6) b = BTree(val=1, left=d, parent=a) c = BTree(val=2, right=g, parent=a) d.parent = b g.parent = c a.left = b a.right = c print(getNext(d).val)
树结构很简单,不说了。