zoukankan      html  css  js  c++  java
  • 《剑指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) != ''){
                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)

    树结构很简单,不说了。

  • 相关阅读:
    ping 网关不同的原因(51cto博客搬迁)
    Linux Shell常用命令总结(51cto 博客搬迁)
    学生管理系统管理系统
    offset属性在各版本浏览器中的不同算法
    常用正则表达式
    select城市3级联动
    html5表单验证消息框
    清除浏览器默认选择效果
    排序之简单的冒泡排序
    简单的桶排序
  • 原文地址:https://www.cnblogs.com/eenio/p/12292567.html
Copyright © 2011-2022 走看看