【剑指offer】1-10题思路详解+Python代码


数组和字符串是两种最基本的的数据结构,它们用连续内存分别存储数字和字符。链表和树是面试中出现频率最高的数据结构,栈是一个与递归紧密相关的数据结构,队列与广度优先遍历算法紧密相关。

数组

最简单的一种数据结构,它占据一块连续的内存并按照顺序存储数据。创建数组时,我们需要首先指定数组的容量大小,然后根据大小分配内存。即使我们只在数组中存储一个数字,也需要为所有的数据预先分配内存。因此数组的空间效率不是很好,经常会有空闲的区域没有得到充分利用。

由于数组中的内存是连续的,于是可以根据下标在O(1)时间读/写任何元素,因此它的时间效率是很高的。根据这一优点,用数组实现简单的哈希表:把数组的下标设为哈希表的键值(Key),而把数组中的每一个数字设为哈希表的值(Value),这样每一个下标及数组中该下标对应的数字就组成了一个“键值-值”的配对。

为了解决数组空间效率不高的问题,人们又设计实现了多种动态数组,比如c++的STL中的vector。为了避免浪费,我们先为数组开辟较小的空间,然后往数组中添加数据。当数据的数目超过数组的容量时,我们再重新分配一块更大的空间(新容量是前一次的两倍),把之前的数据复制到新的数组中,再把之前的内存释放,这样就能减少内存的浪费。但我们也注意到每一次扩充数组容量时都有大量的额外操作,这对时间性能有负面影响,因此使用动态数组时要尽量减少改变数组容量大小的次数。

3.找出数组中重复的数字

在一个长度为n的数组里的所有数字都在0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知福每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或者3.

  • 解题思路:

解决这个问题的一个简单的方法是先把输入的数组排序,从排序的数组中找出重复的数字是一件很容易的事情。排序一个长度为n的数组需要O(nlogn)的时间。

还可以利用哈希表来解决这个问题,从头到尾按顺序扫描数组的每个数字,没扫描到一个数字的时候,都可以用O(1)的时间来判断哈希表里是否已经包含了该数字。如果哈希表里还没有这个数字,就把它加入哈希表。如果哈希表里已经存在这个数字,就找到一个重复的数字。这个算法的时间复杂度是O(n),但它提高时间效率是一个大小为O(n)的哈希表为代价的。

我们注意到数组中的数字都在0~n-1的范围内,如果这个数组中没有重复的数字,那么当数组排序之后数字i将出现在下标为i的位置。由于数组中有重复的数字,有些位置可能存在多个数字,同时有些位置可能没有数字。

现在让我们重排这个数组,从头到尾依次扫描这个数组中的每个数字,当扫描到下标为i的数字时,首先比较这个数字(用m表示)是不是等于i。如果是,则接着扫描下一个数字,如果不是,则在拿他和第m个数字进行比较。如果它和第m个数字相等,就找到了一个重复的数字;如果它和第m个数字不相等,就把第i个数字和第m个数字交换,把m放到属于它的位置。接下来在重复这个比较。

class Solution:
    # 这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
    # 函数返回True/False
    def duplicate(self, numbers, duplication):
        for i in range(len(numbers)):   #注意这一句的用法
            if numbers[i]!=i:
                m=numbers[i]
                if m==numbers[m]:
                    duplication[0]=m
                    return True
                    return duplication[0]
                else:
                    n=numbers[m]
                    numbers[i]=n
                    numbers[m]=m
        return False

不修改数组找出重复的数字

在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。

  • 解题思路:

题目要求不能修改输入的数组,我们可以创建一个长度为n+1的辅助数组,然后逐一把原数组的每个数字复制到辅助数组。如果元数组中被复制的数字是m,则把它复制到辅助数组中下标为m的位置。这样就很容易就能发现那个数字是重复的,由于需要创建一个数组,该方案需要O(n)的辅助空间。

我们把从1n的数字从中间的数字m分为两部分,前面一半为1m,后面一半为m+1n.如果1m的数字的数目超过m,那么这一半的区间里一定包含重复的数字;否则,另一半m+1~n的区间里一定包含重复的数字。我们可以继续把包含重复数字的区间一分为二,直到找到一个重复的数字。但是这个方法无法总统计所有重复的数字。

4.二维数组中的查找

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。完成一个函数,输入一个这样的一个二维数组和一个整数,判断数组中是否含有该整数。

首先选取数组中右上角的数字,如果该数字等于要查找的数字,则查找过程结束;如果该数字大于要查找的数字,则踢出这个数字所在的列;如果该数字小于要查找的数字,则剔除这个数字所在的行。

算法注意点:

  • 求二维列表的行和列的长
x = np.array([[1,2,5],[2,3,5],[3,4,5],[2,3,6]])
# 输出数组的行和列数
print x.shape  # (4, 3)
# 只输出行数

上边是一种做法,但是实例给出的是二维列表,所以说用求列表长度的方式求出行,再用求元素长度的方式求出列

class Solution:
    # array 二维列表
    def Find(self, target, array):
        if not array:
            return False
        r=len(array)
        c=len(array[0])
        i=0
        j=c-1
        while i<r and j>-1:
            temp=array[i][j]
            if temp>target:
                j=j-1
            elif temp<target:
                i=i+1
            elif temp==target:
                return True
        return False

字符串

5.替换空格

实现一个函数,把字符串中的每个空格替换成“%20”。

在网络编程中,如果URL参数中含有特殊字符,如空格、‘#’等,则可能导致服务器段无法获得正确的参数值,我们需要将这些特殊符号转换成服务器可以识别的字符。转化的规则是在’%'后面跟上ASCII码的两位十六进制的表示。比如空格的ASCII码是32,即十六进制的0x20,因此空格被替换为‘%20’。

看到这个题目,我们首先应该想到的是原来一个空格字符,替换之后变成3个字符,字符串变长。如果是在原来的字符串上进行替换,就有可能覆盖修改在该字符串后面的内存。如果是创建新的字符串并在新的字符串上进行替换,那么我们可以自己分配足够多的内存。

假设字符串的长度是n。对每个空格字符,需要移动后面O(n)个字符,因此对于含有O(n)个字符的字符串来说,总的时间效率是O(n^2)

[外链图片转存失败(img-9Dd7sam2-1565227905037)(D:/hc/pictures/jz-1.png)]

a. 把第一个指针指向字符串的末尾,把第二个指针指向替换之后的字符串的末尾;
b.依次复制字符串的内容,直至第一个指针向前移动一格;c.把第一个空格替换成“%20”,把第一个指针向前移动1格,把第二个指针向前移动3格;d.依次向前复制字符串中的字符,直至碰到空格;e.替换字符串中的倒数第二个空格,把第一个指针向前移动1格,把第二个指针向前移动3格。

  • 举一反三
    在合并两个数组(包括字符串)时,如果从前往后复制每个数字(或字符)则需要重复移动数字(或字符)多次,考虑从后往前复制,便可减少移动的次数。

链表

链表室友指针把若干个节点连接成链状结构,动态数据结构,插入一个节点时,只需要为新节点分配内存。

6.从尾到头打印链表

输入一个链表的头结点,从尾到头反过来打印出每个节点的值。

很自然的可以想到把链表中链接节点的指针反转过来,改变链表的方向,但这样就改变了原来链表的结构。

使用insert进行头插法

  • 算法注意点
  1. 链表的定义:
class ListNode:
    def __init__(self,x):
        self.val=x
        self.next=None
  1. 在进行链表的循环时,先建立一个类的实例p,然后运用它的属性p.val,p.next
class Solution:
    # 返回从尾部到头部的列表值序列,例如[1,2,3]
    def printListFromTailToHead(self, listNode):
        if not listNode:
            return []
        p=listNode
        l=[]
        while p:
            l.insert(0,p.val)
            p=p.next
        return l

反转链表

输入一个链表,反转链表后,输出新链表的表头。

  • 算法思想:
    已知原链表头结点phead。新建一个节点pre,当做新链表的头结点。要反转,先把phead.next保存到temp,然后把phead插入到pre前边:phead.next=pre。接着把pre和phead都向前推一格,pre=phead,phead=temp。再继续上述步骤。

  • 算法注意点:
    注意只有一个结点和空链表的情况,返回phead;
    整个循环最后返回的是pre,这才是新链表的头结点,phead最后为空了

# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None
class Solution:
    # 返回ListNode
    def ReverseList(self, pHead):
        if not pHead or not pHead.next:
            return pHead
        pre=None
        while pHead:
            temp=pHead.next
            pHead.next=pre
            pre=pHead
            pHead=temp
        return pre

树的操作会涉及到大量的指针,面试提到的树,大部分是二叉树。二叉树最重要的操作莫过于遍历,即按照某一顺序访问树中的所有节点。有前序、中序、后序三种遍历方式,三种遍历都有递归和循环两种实现方式。

宽度优先遍历:先访问输的第一层节点,再访问树的第二层节点……一直访问到最下面的一层节点。在同一层的节点中,从左到右访问。可以对包括二叉树在内的所有树进行宽度优先遍历。

二叉树的特例二叉搜索树,左子节点总是小于或等于根节点,而右子节点总是大于或等于根节点。

二叉树的另外两个特例是堆和红黑树,堆分为最大堆和最小堆。在最大堆中根节点的值最大,在最小堆中根节点的值最小。有很多需要快速找到最大值或者最小值的问题都可以用堆来解决。

红黑树是把树中的节点定义为红、黑两种颜色,并通过规则确保从根节点到叶节点的最长路径的长度不超过最短路径的两倍。

7.重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,重建该二叉树。例如:输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}

  • 树的定义:
class TreeNode:
    def __init__(self,x):
        self.val=x
        self.left=None
        self.right=None
  • 算法注意点:
    采用递归的方法。在创建类的实例时会自动调用__ init 方法,参数可以通过 init __传递到实例化操作上:
class c:
	def __init__(self,r,i):
		self.r=r
		self.i=i
x=c(3.0,-4.5)
print(x.r,x.i)
  • 注意列表右边界取得是前一个数,例如取前三位,则list[:4]
class Solution:
    # 返回构造的TreeNode根节点
    def reConstructBinaryTree(self, pre, tin):
        if not pre or not tin:
            return None
        root=TreeNode(pre[0])
        val=tin.index(pre[0])
        root.left=self.reConstructBinaryTree(pre[1:val+1],tin[:val])
        root.right=self.reConstructBinaryTree(pre[val+1:],tin[val+1:])
        return root

8.二叉树的下一个节点

给定一颗二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左右子节点的指针,还有一个指向父节点的指针。

栈和队列

栈在计算机领域被广泛应用,比如操作系统会给每个线程创建一个栈用来存储函数调用时各个函数的参数、返回地址及临时变量等。栈的特点是后进先出,即最后被压入栈的元素会第一个被弹出。

通常栈是一个不考虑排序的数据结构,我们需要O(n)时间才能找到栈中的最大或者最小的元素。如果想在O(1)的时间得到栈的最大值或者最小值,则需要对栈做特殊的设计。

队列的特点是先进先出,即第一个进入队列的元素将会第一个出来

9.用两个栈实现队列

用两个栈来实现一个队列,完成队列的Push和Pop操作。队列中的元素为int类型。

这道题的意图是要求我们操作这两个“先进后出”的栈实现一个“先进先出”的队列。

两个栈stack1和stack2,元素一直添加至stack1中,即完成插入操作。在stack2为空时,将stack1中的全部元素弹出到stack2中,然后从stack2弹出完成删除操作;在stack2不为空时,将stack2顶部元素弹出,即完成了队列的一次删除操作。

  • 算法注意点:
  1. 在类的__init __()实例中添加self.stack1() self.stack2()两个属性,保证调用类的实例时就会产生两个列表。
  2. 一定要注意要把stack1中的全部元素都弹出到stack2中,才能删除
  3. 在删除时,注意判断stack1是否为空
class Solution:
    def __init__(self):
        self.stack1=[]
        self.stack2=[]
    def push(self, node):
        self.stack1.append(node)
    def pop(self):
        if self.stack2:
            return self.stack2.pop()
        elif not self.stack1:
            return None
        else:
            while self.stack1:
                self.stack2.append(self.stack1.pop())
            return self.stack2.pop()

算法和数据操作

排序和查找是面试时考察算法的重点。重点掌握二分查找,归并排序和快速排序。

如果面试题要求在二维数组(迷宫或棋盘等)上搜索路径,那么我们可以尝试用回溯法。通常回溯法很适合用递归的代码实现,限定不可以用递归实现时,用栈来模拟递归的实现。

如果求某个问题的最优解,并且该问题可以分为多个子问题,可以用动态规划。

递归和循环

10.斐波那契数列

求斐波那契数列的第n项

直接采用递归效率很低:

class Solution:
    def Fibonacci(self, n):
        if n==0 or n==1:
            return n
        else:
            while n<=39:
                return self.Fibonacci(n-1)+self.Fibonacci(n-2)

上述代码之所以慢,是因为重复的计算太多,我们只要想办法避免重复计算就可以了。例如把已经得到的中间项保存下来,下次需要计算时先查找一下。

class Solution:
    def Fibonacci(self, n):
        a=[0,1]
        while len(a)<=n:
            a.append(a[-1]+a[-2])
        return a[n]

青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

a.如果两种跳法,1阶或者2阶,那么假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1);
b.假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2)
c.由a\b假设可以得出总跳法为: f(n) = f(n-1) + f(n-2)
d.然后通过实际的情况可以得出:只有一阶的时候 f(1) = 1 ,只有两阶的时候可以有 f(2) = 2
e.可以发现最终得出的是一个斐波那契数列:

class Solution:
    def jumpFloor(self, number):
        a=[0,1,2]
        while len(a)<=number:
            a.append(a[-1]+a[-2])
        return a[number]

变态青蛙跳

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

解题思路:
n阶台阶,一次跳上1阶,则还有f(n-1)种跳法,一次跳上2阶,还有f(n-2)种跳法……
f(n)=f(n-1)+f(n-2)+……f(1)
f(n-1)=f(n-2)+f(n-3)+……+f(1)
f(n)=2f(n-1)等比数列
求得f(n)=2^(n-1)

class Solution:
    def jumpFloorII(self, number):
        if number==0:
            return None
        return 2**(number-1)

矩形覆盖

我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2 * 1的小矩形无重叠地覆盖一个2 *n的大矩形,总共有多少种方法?

class Solution:
    def rectCover(self, number):
        a=[0,1,2]
        while len(a)<=number:
            a.append(a[-1]+a[-2])
        return a[number]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值