《剑指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)

树结构很简单,不说了。

 

posted @ 2020-02-11 17:08  _Ennio  阅读(195)  评论(0编辑  收藏  举报