> 文档中心 > 剑指offer(python实现)个人总结参考

剑指offer(python实现)个人总结参考


1. 丑数

题目:把只包含因子2、3和5的数称为丑数。求按从小到大的顺序的第1500个丑数。习惯上把1当作第一个丑数。

  • 蛮力法

    def is_ugly(number):while number%2 == 0number /= 2while number%3 == 0number /= 3while number%5 == 0number /= 5return number == 1? true: false
  • 用空间换时间

    思想:不再像蛮力法那样去依次判断每个数是否为丑数,而是按顺序依次算出下一个丑数。因为每一个丑数都是从已经存在的丑数列表中乘2或乘3或乘5得到。要确保列表中的丑数是排好序的,每次在列表中都会存在一个T2,T3,T5,它们各自前面的丑数乘以2,3,5肯定小于当前丑数列表中的最大值,在它们之后的又会过于大于当前丑数列表的最大值。假设T2x2 = M2,T3x3 = M3,T5x5 = M5。Min(M2,M3,M5)所得即为按顺序的下一个丑数。

    def mins(a, b, c):    a = a if a <= b else b    a = a if a <= c else c    return a# 核心思想:依次算出下一个丑数存入列表中def get_ugly_number(total_counts):    if total_counts <= 0: return 0    ugly_arr = [1]    # 乘2, 乘3,乘5的下标索引    index_2 = 0    index_3 = 0    index_5 = 0    count = 1    while count < total_counts: while ugly_arr[index_2]*2 <= ugly_arr[-1]:     index_2 += 1 while ugly_arr[index_3]*3 <= ugly_arr[-1]:     index_3 += 1 while ugly_arr[index_5]*5 <= ugly_arr[-1]:     index_5 += 1 min_num = mins(ugly_arr[index_2] * 2, ugly_arr[index_3] * 3, ugly_arr[index_5] * 5) ugly_arr.append(min_num) count += 1    target_num = ugly_arr[-1]    ugly_arr.clear()    return target_numif __name__ == '__main__':    print(get_ugly_number(1500))

2. 第一次只出现一次的字符

字典依次存储每个字符在字符串中出现的次数。

def first_not_repeating(s):    if s is None or len(s) == 0: print("输入的参数为空") return None    char_dict = dict()    len_s = len(s)    i = 0    while i < len_s: if s[i] in char_dict:     char_dict[s[i]] += 1 else:     char_dict[s[i]] = 1 i += 1    for c, count in char_dict.items(): if count == 1:     return c    print("不存在只出现过一次的字符")    return Noneif __name__ == '__main__':    strs = 'abaccdeff'    print(first_not_repeating(strs))

3. 从1到n整数中1出现的次数

题目描述:求出1-13的整数中1出现的次数,并算出1-1300的整数中1出现的次数?

  • 蛮力法:思路很简单,但是对于数字n,有O(logn)位,n个数字依次判断的时间复杂度为O(nlogn),计算量比较大。
  • 寻找规律

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BnPWjULN-1648472524107)(D:\Typora_files\整数中1的个数.png)]

def number_of_1_between_1_and_n_solution(n):    count = 0    base = 1    round = n    while round != 0: weight = round % 10 round //= 10 former = n % base if weight > 1:     count += (round*base+base) elif weight == 1:     count += (round*base+former+1) else:     count += (round*base) base *= 10    return countif __name__ == '__main__':    print(number_of_1_between_1_and_n_solution(134))

4. 数值的整数次方(递归法)

题目描述:给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

"""对于exponent的值为负数的情况,可能除2后会多进行一次(1/base)次方操作(如-3//2=-2)但是最后对exponent取余时,会重新乘会(base)--->(如-3%2=1)"""def power(base, exponent):    if base == 0: return base    if exponent == 0: return 1    if exponent == 1: return base    if exponent == -1: return 1/base    half = power(base, exponent//2)    return half*half*power(base, exponent % 2)if __name__ == '__main__':    print(power(2, -3))

5. 数值中的逆序对

题目描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。

  • 蛮力法:顺序扫描整个数组,每扫描到一个数字时,逐个比较该数字与后面的数字的大小关系,统计逆序对的个数,假设数组中有n个数字,则每个数字都要和O(n)个数字做比较,因此,这个暴力解法的时间复杂度为O(n^2)。
  • 归并排序法:首先将数组分隔成子数组,统计出子数组内部逆序对数目,然后再统计相邻子数组之间的逆序对数目,统计过程中还需要对数组进行排序,这实际上就是归并排序的过程。主要考虑的是合并两个有序序列时,计算逆序对数。对于两个有序升序序列,设置两个下标分别指向开始位置,每次比较两个指针对应的值,如果第一个序列当前值大于第二个序列当前值,则有第一个序列“当前长度”个逆序对。

对于参数data,copy的调用顺序,如果在归并后,将归并后的copy的值重新再赋值给data本身,就不需要像下面的代码所展示的,调换实参的位置left = inverse_pairs_core(copy, data, start, start+length)

def inverse_pairs_core(data, copy, start, end):    if start == end: copy[start] = data[start] return 0    length = (end-start) // 2    # copy和data是相互利用的    # 从参数上来看,调用此函数的结果是将排好序的结果存放到copy中,而data是上一步排好的结果。    left = inverse_pairs_core(copy, data, start, start+length)    right = inverse_pairs_core(copy, data, start+length+1, end)    i = start + length    j = end    count = 0    index_copy = end    while i >= start and j >= (start+length+1): if data[i] > data[j]:     copy[index_copy] = data[i]     i -= 1     index_copy -= 1     count += (j-start-length) else:     copy[index_copy] = data[j]     index_copy -= 1     j -= 1    if i >= start: k = i while k >= start:     copy[index_copy] = data[k]     index_copy -= 1     k -= 1    if j >= start+length+1: k = j while k >= start+length+1:     copy[index_copy] = data[k]     index_copy -= 1     k -= 1    return left+right+countdef inverse_pairs(arr):    if arr is None or len(arr) == 0: return 0    copy_arr = arr.copy()    count = inverse_pairs_core(arr, copy_arr, 0, len(arr)-1)    copy_arr.clear()    return countif __name__ == '__main__':    test_arr = [7, 5, 6, 4, 8, 10, 3, 0, 1]    print(inverse_pairs(test_arr))

6. 和为S的两个数字

题目描述:输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vaG7CRqf-1648472524109)(D:\Typora_files\和为S(双指针)].png)

def find_number_s_sum(arr, target_sum):    if arr is None or len(arr) <= 1: print("输入的参数有误") return False    head, tail = 0, len(arr)-1    while head < tail: # 向中间靠拢的过程中,第一次遇到符合要求的两数即为乘积最小的所求目标 if arr[head]+arr[tail] == target_sum:     print(arr[head], '+', arr[tail], '=', target_sum, ',且此时两数乘积最小')     return True elif arr[head]+arr[tail] > target_sum:     tail -= 1 else:     head += 1    return Falseif __name__ == '__main__':    s = [1, 2, 4, 7, 11, 15]    find_number_s_sum(s, 15)

7. 扑克牌顺子

题目描述:

  • 输入的是一个数组,并且长度为5,所有的元素都在0到13的范围内。
  • 判断这5个元素能否构成顺子。
  • 构成顺子的条件是:没有对子、最大最小值间隔不超过4,0可以充当任何数,相当于癞子。
def is_continuous(numbers):    if numbers is None or len(numbers) != 5: print("输入的参数有误") return False    num_0 = 0    numbers.sort()    i = 0    while i < 5: if numbers[i] == 0:     num_0 += 1     i += 1 else:     break    if numbers[4]-numbers[num_0] > 4: return False    t = num_0    while t < 4: if numbers[t] == numbers[t+1]:     return False t += 1    # 若要输出顺子的序列,才需要计算非0数之间的空隔数,来确定最大顺子和最小顺子    return Trueif __name__ == '__main__':    number = [0, 5, 4, 2, 3]    if is_continuous(number): print("输入内容为五顺")    else: print("输入内容不是五顺")

8. 圆圈中最后剩下的数

题目描述:0, 1,…,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。约瑟夫环

方法一:环形链表模拟圆圈

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sztK25d6-1648472524110)(D:\Typora_files\链表圆圈.png)]

缺点:要模拟整个游戏过程,时间复杂度高达O(nm),当n,m非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间内出结果的。

方法二:公式递推法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X5dEgoMQ-1648472524111)(D:\Typora_files\约瑟夫环递推公式.png)]

  • f(N,M)表示,N个人报数,每报到M时杀掉那个人,最终胜利者的编号
  • f(N−1,M)表示,N-1个人报数,每报到M时杀掉那个人,最终胜利者的编号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6pSpHTZh-1648472524112)(D:\Typora_files\约瑟夫环实例.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xkZ0JMC1-1648472524113)(D:\Typora_files\约瑟夫环公式解析.png)]

注:理解这个递推式的核心在于关注胜利者的下标位置是怎么变的。每杀掉一个人,其实就是把这个数组向前移动了M位。然后逆过来,就可以得到这个递推式。因为求出的结果是数组中的下标,最终的编号还要加1。

# 约瑟夫环问题def last_remaining(n, m):    if n < 1 or m < 1: return -1    last = 0    i = 2    while i <= n: last = (last + m) % i i += 1    return last    # return last+1 若编号是从1开始的,则最后留下的人为last+1(下标加1)if __name__ == '__main__':    print(last_remaining(5, 3))

9. 求1+2+3+…+n的值

题目描述:求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)

分析:可以通过递归来实现加法,但是由于无法使用if语句,因此对于递归的结束条件无法进行判断,这里用一个比较巧妙的思路:与运算的短路特性,所谓短路,比如 A && B,当A条件不成立时,不论B是否成立,结果都是false,所以B不再进行计算,利用短路特性可以实现递归停止,进而求出和。

#include#include int sum_1_to_n(int n){int sum = n;bool ans;ans = (n > 0) && ((sum += sum_1_to_n(n-1)) > 0);return sum;}int main(void){int n = 10;printf("累加和为%d", sum_1_to_n(n));} 
# 海象运算符python3.8版本才引入----》  ":="def sum_1_to_n(n):    sums = n    ans = (n > 0) and (sums :=sum_1_to_n(n-1) > 0)    return sumsif __name__ == '__main__':    print(sum_1_to_n(9))

10. 和为S的连续正数序列

题目描述:输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序。

解析:由于是在一个连续序列中连续查找,可以使用类似滑动窗口的思想,使用双指针定位滑动窗口的上下边界,用两个数low和high分别指向当前序列中的最大和最小值,初始low为1,high为2。如果从low到high的序列的和大于给定的S,那么说明可以去掉一个比较小的值,即增大low的值(相当于去掉了一个最小值,窗口收缩)。反之,如果从low到high的序列和小于给定的S,则应该增加一个值,即增大high(相当于窗口扩张,让这个窗口包含更多的值)。这样依次查找就可以找到所有的满足条件的序列,并且符合序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序要求。另外,需要注意的是:循环的结束条件:由于要求序列至少包含两个数,因此当low追上high或者当low超过S的一半时,即可停止查找。顺着low,high指针值递增的方向进行滑窗

def find_continuous_sequence(sums):    if sums < 3: print("无连续序列") return None    low, high = 1, 2    sequence_list = []    cur_sums = low + high    # 顺着指针值递增的方向进行    while low < high and low < (sums+1) // 2: if cur_sums == sums:     sequence_list.append((low, high))     # 此时采用移动low指针的方法可以减少移动次数     # high += 1     # cur_sums += high     cur_sums -= low     low += 1 elif cur_sums > sums:     cur_sums -= low     low += 1 else:     high += 1     cur_sums += high    return sequence_listif __name__ == '__main__':    print(find_continuous_sequence(100))

11. 二进制中1的个数

题目描述:输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

解析:

  • 依次判断每一位。判断的方法是先与1相与,为1则说明该位为1,为0说明该位为0,然后将1左移,再判断倒数第二位,依次循环32次。(注意这里使用1左移,而不要让该数右移,右移可能会因符号位的问题而导致死循环,一般情况下能用左移尽量不用右移)。
  • n与n-1相与,直到相与结果变为0。如果n的最右一位为1的话,n-1除了最右位变为0其他位同n相同,相与去掉最右边的1;如果n的最右边的1不在最右位,那么n-1相对于n而言,n-1的该位变为0,而这个位右边的全变为1;因此n不论是最右的1位在哪,它和n-1的&运算将会让最右的1变为0,而这个最右1位的左边不变。即做一次&,n的1的位数减1,这时n的值也变了,因此一直&到n变为0,我们即可得出n的1的个数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hQX002oh-1648472524114)(D:\Typora_files\二进制1的个数.png)]

def number_of_1(n):    count = 0    while n > 0: if (n & 1) == 1:     count += 1 n = n >> 1    return countdef number_of_1_(n):    count = 0    while n > 0: count += 1 n = n & (n-1)    return countif __name__ == '__main__':    print(number_of_1(7))    print(number_of_1_(8))

12. 不用加减乘除做加法

def add(n1, n2):    n1 &= 0XFFFFFFFF    n2 &= 0XFFFFFFFF    while n2 != 0: carry = n1 & n2 n1 ^= n2 n2 = (carry << 1) & 0XFFFFFFFF    return n1 if n1 < 0X80000000 else ~(n1 ^ 0XFFFFFFFF)if __name__ == '__main__':    print(add(0, -1))

13. 斐波那契数列

题目描述:第一项是0,第二项是1,后续第n项为第n-1项和第n-2项之和。

可以想到直接用递归解决。但是这里存在一个效率问题,以求f(10)为例,需要先求出前两项f(9)和f(8),同样求f(9)的时候又需要求一次f(8),这样会导致很多重复计算。重复计算的结点数会随着n的增加而急剧增加,导致严重的效率问题。 因此,可以不使用递归,直接使用简单的循环方法实现。

def fibonacci(n):    if n == 0: return 0    if n == 1: return 1    # return fibonacci(n - 1) + fibonacci(n - 2)    result = 0    a, b = 1, 0    i = 2    while i <= n: result = a + b a, b = result, a i += 1    return resultif __name__ == '__main__':    print(fibonacci(10))

14. 跳台阶

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

解析:首先考虑最简单的情况,如果只有1级台阶,显然只有一种跳法。如果有两级台阶,就有两种跳法:一种是分两次跳,一种是一次跳两级。在一般情况下,可以把n级台阶的跳法看成n的函数,记为f(n),那么一般情况下,一开始我们有两种不同的选择:(1)第一步只跳一级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即f(n-1);(2)第一步跳两级,那么跳法数目等于后面剩下的n-2级台阶的跳法数目,即f(n-2)。所以f(n)=f(n-1)+f(n-2)。

def jump_floor_i(n):    if n <= 0: print("输入参数不正确") return -1    if n == 1: return 1    if n == 2: return 2    first, second = 1, 2    result = 0    i = 3    while i <= n: result = first + second first, second = second, result i += 1    return resultif __name__ == '__main__':    print(jump_floor_i(5))

15. 变态跳台阶

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

解析:当只有一级台阶时,f(1)=1;当有两级台阶时,f(2)=f(2-1)+f(2-2);一般情况下,当有n级台阶时,f(n)=f(n-1)+f(n-2)+···+f(n-n)=f(0)+f(1)+···+f(n-1),同理,f(n-1)=f(0)+f(1)+···+f(n-2)。因此,根据上述规律可以得到:f(n)=2*f(n-1)。这时一个递推公式,同样为了效率问题,用循环可以实现。

def jump_floor_ii(n):    if n <= 0: print("输入的参数有误") return -1    if n == 1: return 1    i = 2    result = 1    while i <= n: result = 2 * result i += 1    return resultif __name__ == '__main__':    print(jump_floor_ii(5))

16. 矩阵覆盖

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1XV275C-1648472524115)(D:\Typora_files\矩阵覆盖.png)]

解析:先把2X8的覆盖方法记为f(8),用1X2的小矩形去覆盖时,有两种选择:横着放或者竖着放。当竖着放时,右边还剩下2X7的区域。很明显这种情况下覆盖方法为f(7)。当横着放时,1X2的矩形放在左上角,其下方区域只能也横着放一个矩形,此时右边区域值剩下2X6的区域,这种情况下覆盖方法为f(6)。所以可以得到:f(8)=f(7)+f(6),不难看出这仍然是斐波那契数列。特殊情况:f(1)=1,f(2)=2

def rect_cover(n):    if n <= 0: print("输入的参数不正确") return -1    if n <= 2: return n    i = 3    first, second = 1, 2    result = 0    while i <= n: result = first + second first, second = second, result i += 1    return resultif __name__ == '__main__':    print(rect_cover(4))

17. 用两个栈实现队列

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

解析:有两个栈stack1和stack2,每次向队列中插入元素可以都压入到stack1中,当需要从队列中删除元素时,我们应该是删除最早插入的那个(FIFO),这时可以将stack1中的元素逐个弹出并压入stack2,直到stack1为空,这时最早插入的元素就位于stack2的栈顶,可以直接弹出。总结如下:压入元素时,都压入栈1,当需要弹出时,从栈2弹出,当栈2不为空时直接弹出栈顶元素,为空时将栈1的元素“倒进去”。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-09srCZra-1648472524117)(D:\Typora_files\两个栈实现队列.png)]

from common import MyStackclass MyQueue:    def __init__(self): self.s1 = MyStack() self.s2 = MyStack()    def is_empty(self): return self.s1.is_empty() and self.s2.is_empty()    def push(self, node): self.s1.push(node)    def pop(self): if self.is_empty():     print("当前队列为空")     return None if self.s2.is_empty():     while not self.s1.is_empty():  self.s2.push(self.s1.pop()) return self.s2.pop()if __name__ == '__main__':    queue = MyQueue()    queue.push(1)    queue.push(2)    queue.push(3)    queue.push(4)    print(queue.pop())    print(queue.pop())    print(queue.pop())    print(queue.pop())    print(queue.pop())

18. 包含min函数的栈

题目描述:定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。

解析:

  • 解法一:使用两个stack,一个为数据栈,另一个为辅助栈。数据栈用于存储所有数据,每次压栈的最小元素(之前的最小元素和新压入栈中的元素,二者的较小值)保存起来放入辅助栈。
  • 解法二:最小元素每一次都进栈,会有重复,也可以只有当进栈元素小于或等于之前的最小元素时,最小元素才进栈,而当弹出元素等于最小元素时,最小元素才出栈,从而节省一部分空间。
  • 解法三:定义一个组合变量,比如Node,包括数据和最小元素两个成员变量,从而节省一个栈,但是本质仍然没有变。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PAMRoJ4R-1648472524119)(D:\Typora_files\包含min函数的栈(例)].png)

from common import MyStack# 两栈实现,数据栈和辅助栈,数据栈正常存储进栈出栈的数据,辅助栈记录每次栈有变化时,最新的最小值。class Test:    def __init__(self): self.data_stack = MyStack() self.help_stack = MyStack()    def is_empty(self): return self.data_stack.is_empty()    def push(self, item): self.data_stack.push(item) if self.help_stack.is_empty():     self.help_stack.push(item) else:     temp = self.help_stack.top()     if temp > item:  temp = item     self.help_stack.push(temp)    def pop(self): self.help_stack.pop() return self.data_stack.pop()    def get_min(self): if self.help_stack.is_empty():     print("此时栈为空")     return None return self.help_stack.top()if __name__ == '__main__':    test = Test()    test.push(3)    print(test.get_min())    test.push(4)    print(test.get_min())    test.push(2)    print(test.get_min())    test.push(1)    print(test.get_min())    test.pop()    print(test.get_min())    test.pop()    print(test.get_min())    test.push(0)    print(test.get_min())

19. 栈的压入、弹出序列

题目描述:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

解析:找到判断一个序列是不是栈的弹出序列的规律:根据弹出序列,我们依次进行判断,如果下一个要弹出的数字刚好是栈顶数字,那么直接弹出;如果下一个要弹出的数字不在栈顶,则把压栈序列中还没有入栈的数字压入栈中,直到将下一个要弹出的数字压入栈顶为止,如果所有的数字都压入栈中后仍然没有找到下一个要弹出的数字,那么该序列就不可能是一个弹出序列。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ZhM4oEm-1648472524120)(D:\Typora_files\压入栈、弹出序列(例)].png)

from common import MyStackdef if_pop_sequence(a, b):    if a is None or b is None: print("输入的参数有误") return False    len_a = len(a)    len_b = len(b)    if len_a != len_b: print("入栈序列和出栈序列的长度不相同") return False    stack = MyStack()    # i,j各自表示入栈和出栈计数(减一)    i, j = 0, 0    stack.push(a[i])    while True: if stack.top() == b[j]:     stack.pop()     j += 1     if j == len_b:  break else:     i += 1     if i == len_a:  break     stack.push(a[i])    if stack.is_empty(): return True    return Falseif __name__ == '__main__':    s1 = [1, 2, 3, 4, 5]    # s2 = [4, 5, 3, 2, 1]    # s2 = [4, 3, 5, 1, 2]    s2 = [6, 5, 4, 3, 2]    if if_pop_sequence(s1, s2): print("可得出该出栈序列")    else: print("该出栈序列不可得到")

20. 矩阵中的路径

题目描述:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

解析:回溯法,就是有组织的进行穷举搜索的过程,通过深度优先来对所有可能的解进行穷举。

​ 首先,遍历整个矩阵,我们可以找到和字符串str首字符相同的单元格,作为查找的起点,然后遍历它的上下左右四个字符,如果有和str下一个字符相同的,就以该单元格作为下一个遍历起点,依次进行,如果没有找到就回退到上一个字符,重新查找其他方向。整个过程是一个非常典型的回溯思路。由于回溯法天生的递归特性,路径可以看做一个,当在矩阵中定位了路径中前n个字符的位置后,在第n个字符的上下左右(没有遍历过的)格子找第n+1个字符,如果没有找到,只能回退到第n-1个字符,重新定位第n个字符。由于路径不能重复进入矩阵的格子,所以还需要一个和矩阵大小相同的布尔矩阵来标识每个单元格是否被访问过,这也是回溯法的惯用方法。

class Test:    def __init__(self, row, col): self.flags = [[0] * col for _ in range(row)] self.row = row self.col = col    def has_path_detail(self, mat, s, i, j, start): if start == len(s):  # s中元素已经遍历完     return True if i < 0 or i >= self.row or j < 0 or j >= self.col:  # i, j越界     return False res = False # 如果相等并且未被访问过 if mat[i][j] == s[start] and self.flags[i][j] == 0:     self.flags[i][j] = 1     start += 1     # 上下左右四个方向寻找     res = self.has_path_detail(mat, s, i - 1, j, start) or self.has_path_detail(mat, s, i, j - 1, start) \    or self.has_path_detail(mat, s, i + 1, j, start) or self.has_path_detail(mat, s, i, j + 1, start)     if not res:  # 没找到,进行回溯  start -= 1  self.flags[i][j] = 0 return res    def has_path(self, mat, s): i, j = 0, 0 while i < self.row:     while j < self.col:  # 以mat[i][j]为初始点进行搜索  if self.has_path_detail(mat, s, i, j, 0):      return True  j += 1     i += 1 return Falseif __name__ == '__main__':    s1 = ['b', 'c', 'c', 'e', 'd']    s2 = ['a', 'b', 'c', 'b']    matrix = [['a', 'b', 'c', 'e'], ['s', 'f', 'c', 's'], ['a', 'd', 'e', 'e']]    test = Test(3, 4)    if test.has_path(matrix, s2): print('矩阵中存在一条包含字符串所有字符的路径')    else: print('矩阵中不存在一条包含字符串所有字符的路径')

21. 机器人是运动范围

题目描述:地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

解析:回溯法解决,同题目20。

class Test:    def __init__(self, row, col, threshold): self.row = row self.col = col self.threshold = threshold self.flags = [[False]*col for _ in range(row)]    def moving_count(self, i, j): count = 0 if self.check(i, j):     self.flags[i][j] = True     count = 1 + self.moving_count(i-1, j) + self.moving_count(i, j-1) \      + self.moving_count(i+1, j) + self.moving_count(i, j+1) return count    def check(self, i, j): if i < 0 or j < 0 or i >= self.row or j >= self.col:     return False if (self.get_num(i) + self.get_num(j)) <= self.threshold and not self.flags[i][j]:     return True return False    def get_num(self, num): sums = 0 while num != 0:     sums += (num % 10)     num //= 10 return sums    if __name__ == '__main__':    test = Test(10, 10, 5)    print(test.moving_count(0, 0))    print(test.flags)

22. 从尾到头打印链表

题目描述:输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。

解析:(三种方法:借助栈、递归、列表的首位插入)从头到尾打印链表比较简单,从尾到头很自然的可以想到先将链表进行反转,然后再打印。但是,通常我们不希望改变原链表的结构,这是一个只读操作。因此,我们进一步分析,可以发现排在后面的先输出,这是一个典型的“后入先出”的思想,因此很自然的可以想到用栈来实现,每遍历一个结点,可以将其压入栈中,遍历结束后再逐个弹栈,将结点值存入ArrayList,这样就实现了从尾到头的打印。更进一步,既然想到了用栈,那一定可以通过递归来实现。每访问到一个结点,先递归输出其后的结点,在输出该结点自身即可。另外,当使用Java或者python语言时,有一种比较巧妙的方法就是使用列表的插入方法,每次插入数据,都总是插入到首位,这样得到的List就是从尾到头的链表序列。

from common import construct_list, print_listdef get_reverse_list(head):    if head.next is None: return None    temp1 = head.next    get_next(temp1)def get_next(list1):    if list1 is None: return    get_next(list1.next)    print(list1.data, '  ', end='')if __name__ == '__main__':    temp_head = construct_list()    print('逆序前链表内容为:')    print_list(temp_head)    print('\n逆序后链表内容为:')    get_reverse_list(temp_head)
from common import construct_list, print_list, LNodedef get_reverse_list(head):    if head.next is None: return None    temp1 = head.next    temp2 = None    tail, temp2 = get_next(temp1, temp2)    return temp2# list2表示保存链表反转后的头指针def get_next(list1, list2):    if list1 is None: temp = LNode() list2 = temp return temp, list2    temp, list2 = get_next(list1.next, list2)    node = LNode()    node.data = list1.data    temp.next = node    return node, list2if __name__ == '__main__':    temp_head = construct_list()    print('逆序前链表内容为:')    print_list(temp_head)    print('\n逆序后链表内容为:')    reverse_head = get_reverse_list(temp_head)    print_list(reverse_head)
from common import construct_list, print_list, LNodeclass Test:    def __init__(self): self.result = None    def get_reverse_list(self, head): if head.next is None:     return None temp1 = head.next self.get_next(temp1)    def get_next(self, list1): if list1 is None:     self.result = LNode()     return self.result temp = self.get_next(list1.next) node = LNode() node.data = list1.data temp.next = node return nodeif __name__ == '__main__':    temp_head = construct_list()    print('逆序前链表内容为:')    print_list(temp_head)    print('\n逆序后链表内容为:')    test = Test()    test.get_reverse_list(temp_head)    print_list(test.result)

23. 链表中倒数第K个结点

题目描述:输入一个链表,输出该链表中倒数第k个结点。为了符合习惯,从1开始计数,即链表的尾结点是倒数第1个节点。例如,一个链表有6个结点,从头结点开始,它们的值依次是1,2,3,4,5,6。则这个链表倒数第三个结点是值为4的结点。

解析:对于单链表来说,没有从后向前的指针,因此一个直观的解法是先进行一次遍历,统计出链表中结点的个数n,第二次再进行一次遍历,找到第n-k+1个结点就是要找的结点,但是这需要对链表进行两次遍历。为了实现一次遍历,这里采用双指针解法。可以定义两个指针,第一个指针从链表的头指针开始先向前走k步,第二个指针保持不动,从第k+1步开始,第二个指针也从头开始前进,两个指针都每次前进一步。这样,两个指针的距离都一直保持在k,当快指针(走在前面的)到达null时,慢指针(走在后面的)正好到达第k个结点。注意:要时刻留意空指针的判断。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dBZbmsBe-1648472524121)(D:\Typora_files\链表倒数第K个结点(例)].png)

from common import construct_listdef get_re_k_node(head, k):    if head is None or head.next is None or k <= 0: print("输入的参数有误") return None    fast, low = head, head    i = 1    while i <= k: if fast is not None:     fast = fast.next     i += 1 else:     print("输入的参数k过大")     return None    while fast is not None: fast = fast.next low = low.next    return lowif __name__ == '__main__':    test_head = construct_list()    result = get_re_k_node(test_head, 4)    if result: print("当前链表的第倒数k个结点的值为:", result.data)    else: print("所要求的结点不存在")

24. 反转链表

题目描述:输入一个链表,反转链表后,输出新链表的表头。

解析:有两种方法可以实现:(1)三指针。使用三个指针,分别指向当前遍历到的结点、它的前一个结点以及后一个结点。将指针反转后,三个结点依次前移即可。(2)递归方法。同样可以采用递归来实现反转。将头结点之后的链表反转后,再将头结点接到尾部即可。

//方法一:三指针    public ListNode ReverseList(ListNode head) { if(head==null)     return null; ListNode first=null;  ListNode second=head; ListNode third=head.next; while(third!=null){      second.next=first; //三指针之间的变换     first=second;     second=third;     third=third.next; } second.next=first; return second;    }
from common import construct_list, print_listdef reverse_list_1(head):    if head is None or head.next is None: # head is None 是针对只有头结点的而没有内容结点的时候 # head.next is None 就是对于正常链表的递归结束条件 return head    temp = reverse_list_1(head.next)    head.next.next = head    head.next = None    return tempdef reverse_list_2(head):    if head is None or head.next is None or head.next.next is None: return head    first = head.next.next    second = first.next    head.next.next = None    while True: first.next = head.next head.next = first if second is None:     break first = second second = second.next    return headif __name__ == '__main__':    test_head = construct_list()    print("原链表内容:")    print_list(test_head)    # test_head.next = reverse_list_1(test_head.next)    print("\n反转后的链表内容为:")    print_list(reverse_list_2(test_head))

25. 复杂链表的复制

题目描述:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。

思路:(最优方法):同样先按next复制,但是把复制后的节点放到原节点后面,则可以很容易的添加random,最后按照奇偶位置拆成两个链表,时间复杂度O(n),不需要额外空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QFTjKwgK-1648472524130)(D:\Typora_files\复杂链表_1.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-psaijBvp-1648472524131)(D:\Typora_files\复杂链表_2.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QMwDYsYA-1648472524132)(D:\Typora_files\复杂链表_3.png)]

class Node:    def __init__(self): self.data = None self.next = None self.random = Nonedef copy_node(phead):    p_temp = phead.next    while p_temp is not None: temp = Node() temp.data = p_temp.data temp.next = p_temp.next p_temp.next = temp p_temp = temp.nextdef add_random(phead):    p_temp = phead.next    while p_temp is not None: temp = p_temp.next if p_temp is not None and p_temp.random is not None:  temp.random = p_temp.random.next p_temp = temp.nextdef reconnect_nodes(phead):    if phead is None: return None    clone_head = Node()  # 头指针    # 奇偶数链表各自的第一个结点内容    head1 = phead.next    head2 = head1.next    clone_head.next = head2    while head1 is not None: head1.next = head2.next head1 = head1.next if head1 is not None:     head2.next = head1.next     head2 = head2.next    return clone_headdef copy_complex_list(phead):    if phead is None: return None    copy_node(phead)    add_random(phead)    result = reconnect_nodes(phead)    return resultif __name__ == '__main__':    head = Node()    node1 = Node()    node2 = Node()    node3 = Node()    node4 = Node()    node5 = Node()    node1.data = 'A'    node2.data = 'B'    node3.data = 'C'    node4.data = 'D'    node5.data = 'E'    head.next = node1    node1.next = node2    node2.next = node3    node3.next = node4    node4.next = node5    node1.random = node3    node2.random = node5    node4.random = node2    # ------复杂链表构建完成------    copy_result = copy_complex_list(head)    print('复制结果结点A随机指向:', copy_result.next.random.data)    print('复制结果结点B随机指向:', copy_result.next.next.random.data)    print('复制结果结点D随机指向:', copy_result.next.next.next.next.random.data)

26. 两个链表的第一个公共结点

题目描述:输入两个链表,找出它们的第一个公共结点。

思路:

方法一:将两个链表设置成一样长。具体做法是先求出两个链表各自的长度,然后将长的链表的头砍掉,也就是长的链表先走几步,使得剩余的长度与短链表一样长,这样同时向前遍历便可以得到公共结点。时间复杂度为O(m+n),不需要额外空间。

方法二:将两个链表拼接起来。 将两个链表进行拼接,一个链表1在前链表2在后,另一个链表2在前链表1在后,则合成的两个链表一样长,然后同时遍历两个链表,就可以找到公共结点,时间复杂度同样为O(m+n)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3P2ACSIi-1648472524132)(D:\Typora_files\公共结点方法解析.jpg)]

from common import LNode, print_list# 此方法的前提是必须存在公共结点,否则会陷入无限的循环中def find_first_common_node(phead1, phead2):    if phead1 is None or phead2 is None or phead1.next is None or phead2.next is None: return None    temp1, temp2 = phead1.next, phead2.next    while temp1 != temp2: temp1 = temp1.next if temp1 else phead2.next temp2 = temp2.next if temp2 else phead1.next    return temp1if __name__ == '__main__':    head1, head2 = LNode(), LNode()    node1 = LNode()    node1.data = 1    node2 = LNode()    node2.data = 2    node3 = LNode()    node3.data = 3    node4 = LNode()    node4.data = 4    node5 = LNode()    node5.data = 5    node6 = LNode()    node6.data = 6    node7 = LNode()    node7.data = 7    head1.next = node1    node1.next = node2    node2.next = node3    node3.next = node6    node6.next = node7    head2.next = node4    node4.next = node5    node5.next = node6    print_list(head1)    print('\n', end='')    print_list(head2)    print('\n', end='')    print(find_first_common_node(head1, head2).data)

27. 链表中环的入口结点

题目描述:给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

思路:快慢指针找到相遇点,一个指针从开始出发,另一指针从相遇点出发,再次相遇的点即为环的入口点。(公式所得)

from common import LNode, print_list# 构造链表def construct_list():    i = 1    head = LNode()    head.next = None    cur = head    while i < 8: tmp = LNode() tmp.data = i tmp.next = None cur.next = tmp cur = tmp i += 1    cur.next = head.next.next.next    return headdef is_loop(head):    if head is None or head.next is None: return None    fast, slow = head.next, head.next    while fast is not None and fast.next is not None: slow = slow.next fast = fast.next.next if slow == fast:     return slow    return Nonedef find_entry(head, meet_node):    first = head.next    second = meet_node    while first != second: first = first.next second = second.next    return secondif __name__ == '__main__':    head = construct_list()    meet_node = is_loop(head)    if meet_node is not None: print("此单链表存在环") print("当前环的入口点数据为:", find_entry(head, meet_node).data)    else: print("此链表无环")

28. 删除链表中的重复结点

题目描述:在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。

思路:删除重复结点,也就是如果当前结点和下一个结点的值相同,那么就是重复的结点,都可以被删除,为了保证删除之后的链表的连通性,在删除之后,要把当前结点前面的结点和下一个没有重复的结点链接起来,为此,程序需要记录当前的最后一个不重复结点,即程序中的pre。重点在于:一定要确保当前链接到链表中的一定是不会再重复的结点。

from common import LNode, print_listdef delete_dup_node(head):    if head is None or head.next is None : return None    # cur指针指向当前准备判断的结点位置    # pre指针指向已经保存好的不重复链表的结尾位置    pre, cur = head, head.next    # 当链表的最后一个结点是重复结点时,循环的第一个条件(cur is not None)不满足,循环跳出。    # 当链表的最后一个结点不是重复结点时,循环的第二个条件(cur.next is not None)不满足,循环跳出。    while cur is not None and cur.next is not None: if cur.data != cur.next.data:     pre = cur     cur = cur.next else:     data = cur.data     # 循环找到与当前重复结点第一个不重复的结点的位置     while cur is not None and cur.data == data:  cur = cur.next     pre.next = cur    return headif __name__ == '__main__':    # 链表构建    head = LNode()    node1 = LNode()    node2 = LNode()    node3 = LNode()    node4 = LNode()    node5 = LNode()    node6 = LNode()    node7 = LNode()    node1.data = 1    node2.data = 2    node3.data = 3    node4.data = 3    node5.data = 4    node6.data = 4    node7.data = 5    head.next = node1    node1.next = node2    node2.next = node3    node3.next = node4    node4.next = node5    node5.next = node6    node6.next = node7    print("链表初始内容为:")    print_list(head)    print("\n删除链表重复内容后的结果为:")    head = delete_dup_node(head)    print_list(head)

29. 二维数组中的查找

题目描述:在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路:由于该二维数组上到下递增,左到右递增的特殊性,遍历整个矩阵进行查找不是该题目的意图所在。总结规律我们可以发现:应该从矩阵的右上角或者左下角开始查找。以右上角为例,首先选取右上角的数字,如果该数字等于要查找的数字,则查找过程结束;如果该数字大于要查找的数字,则说明该列其他元素都大于要查找的数字,便可以删掉该列;如果该数字小于要查找的数字,则说明该行其他元素也都小于要查找的数字,便可以删掉该行。这样,每一次比较都可以剔除一行或者一列,进而缩小查找范围,时间复杂度为O(n)。

def find_elements(arr, target):    if arr is None: print("输入数组不合法") return False    row, col = len(arr), len(arr[0])    i, j = 0, col-1    while i < row and j > -1: if arr[i][j] == target:     return True elif arr[i][j] > target:     j -= 1 else:     i += 1    return Falseif __name__ == '__main__':    s = [[1, 2, 8, 9], [2, 4, 9, 12], [4, 7, 10, 13], [6, 8, 11, 15]]    if find_elements(s, -1): print("此元素在二维数组中存在")    else: print("此元素在二维数组中不存在")

30. 旋转数组中的最小数字

题目描述:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 注:给出的所有元素都大于0,若数组大小为0,请返回0。

思路:对于旋转数组,它实际上可以划分为两个排序的子数组,而且前面数组的元素都不小于后面数组的元素,并且最小值正好就是这两个数组的分界线。

​ 首先用两个指针low和high分别指向数组的第一个元素和最后一个元素,然后可以找到中间元素mid。对于这个中间元素,有以下两种情况:(1)该元素大于等于low指向的元素,此时最小的元素说明在mid的后面,可以把low=mid;(2)中间元素小于等于high指向的元素,那么最小元素在mid之前,可以high=mid。注意:这里不要+1或者-1,因为只有这样才能保证low始终在第一个数组,high始终在第二个数组。依次循环,当最后low和high相差1时,low指向第一个数组的最后一个,high指向第二个数组的第一个(即为要找的最小值)。

还有两个特殊情况:

  • 将数组前0个元素移动到后面(相当于没有旋转,数组整体有序)。明显我们上面的分析没有包含这种情况,需要特殊处理,将第一个元素和最后一个元素相比,若第一个元素小于最后一个元素,则说明最小值就是的第一个元素,可以直接返回。
  • 首尾指针指向的数字和中间元素三者都相等时,无法判断中间元素位于哪个子数组,无法缩小问题规模。此时,只能退而求其次,进行顺序查找。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DrA1aOVE-1648472524133)(D:\Typora_files\旋转数组_特例.png)]

def min_in_order(arr):    if arr is None: return -1    min_value = arr[0]    arr_len = len(arr)    i = 1    while i < arr_len: if arr[i] < min_value:     min_value = arr[i] i += 1    return min_valuedef min_number_in_rotate_array(arr):    if arr is None: print("输入的数据为空") return 0    arr_len = len(arr)    if arr_len == 0: return 0    start, end = 0, arr_len - 1    # 该旋转数组就为初始非减排序数组    if arr[start] < arr[end]: return arr[start]    # 实际上找到是旋转数组的两子数组的"分界线"(二分法查找)    while start < end: mid = start + (end-start)//2 if arr[start] == arr[mid] and arr[end] == arr[mid]:     return min_in_order(arr) if arr[mid] >= arr[start]:     start = mid elif arr[mid] <= arr[end]:     end = mid if end-start == 1:     return arr[end]    return -1if __name__ == '__main__':    s1 = [3, 4, 5, 1, 2]    s2 = [1, 2, 3, 4, 5]    s3 = [1, 0, 1, 1, 1]    s4 = [1, 1, 1, 0, 1]    print(min_number_in_rotate_array(s1))    print(min_number_in_rotate_array(s2))    print(min_number_in_rotate_array(s3))    print(min_number_in_rotate_array(s4))

31. 调整数组顺序使奇数位于偶数前面

题目描述:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

解析:

  • 首先,如果不考虑奇数和奇数,偶数和偶数的相对位置,那么有一种双指针解法来求解,类似于快排,维护两个指针,第一个指针指向数组的第一个数字,第二个指针指向数组的最后一个数字。第一个指针向后移,第二个指针向前移,如果第一个指针指向偶数,第二个指针指向的是奇数,则交换着两个数字,接着继续移动直到两指针相遇。
  • 对数组进行遍历,设置两个指针even和odd,even指向当前第一个偶数,odd从这个偶数之后开始查找,找到第一个奇数,此时为了相对位置不变,不能直接交换even和odd,而是将从even到odd-1的元素都依次向后移一个位置,将odd指向的那个奇数放到even的位置。然后再找下一个偶数,重复这一过程,最终就可以将奇数都放到偶数的前面,并且保证了相对位置的不变。
def reorder_array(arr):    arr_len = len(arr)    even, odd = 0, 0    while even < arr_len and odd < arr_len: # 找到第一个偶数 while even < arr_len and arr[even] % 2 != 0:     even += 1 odd = even + 1 # 找到偶数后的第一个奇数 while odd < arr_len and arr[odd] % 2 == 0:     odd += 1 # 越界处理,防止溢出 if odd >= arr_len:     break # 把奇数取出来,从even到odd-1的偶数元素都向后移 temp = arr[odd] i = odd while i > even:     arr[i] = arr[i-1]     i -= 1 arr[even] = temp # 开启下一轮的移动,每次找出一位奇数,将它放到最终的位置上 even += 1if __name__ == '__main__':    s = [1, 2, 3, 4, 5, 6, 7]    print("排序前:", end='')    print(s)    reorder_array(s)    print("排序后:", end='')    print(s)

32. 顺时针打印矩阵

题目描述:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10。

解析:由于是按照从外到内的顺序依次打印,所以可以把矩阵想象成若干个圈,用一个循环来打印矩阵,每次打印矩阵中的一圈。假设矩阵的行数是row,列数是col,则每次都是从左上角开始遍历,而注意到左上角行标和列标总是相同的,该矩阵打印结束的条件就是左上角的元素下标走到了该矩阵行和列的一半时该矩阵也就打印结束了。假设是start,那么循环继续的条件就是row > 2 x start and col > 2 x start。而对于每一圈的打印,很自然便可以想到遵循从左到右,从上到下,从右到左,从下到上的顺序。但是这里需要注意的是最后一圈的打印,由于矩阵并不一定是方阵,最后一圈有可能退化为只有一行,只有一列,甚至只有一个数,因此要注意进行判断,避免重复打印。

def print_matrix_cw(arr):    if arr is None: print("输入的参数有误") return    row, col = len(arr), len(arr[0])    i = 0S    # 循环打印结束的条件    while row > 2*i and col > 2*i: # 打印一圈,右下角的边界坐标 end_x = row - 1 - i end_y = col - 1 - i # 从左到右开始打印 j = i while j <= end_y:     print(arr[i][j], '-', end='', sep='')     j += 1 # 从上到下开始打印 j = i + 1 while j <= end_x:     print(arr[j][end_y], '-', end='', sep='')     j += 1 # 从右向左开始打印 if end_x > i:     j = end_y - 1     while j >= i:  print(arr[end_x][j], '-', end='', sep='')  j -= 1 # 从下向上开始打印 if end_y > i:     j = end_x - 1     while j > i:  print(arr[j][i], '-', end='', sep='')  j -= 1 # 下一圈开始打印(i+1, i+1) i += 1    returnif __name__ == '__main__':    s = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]    print_matrix_cw(s)

33. 数组中出现次数超过一半的数字

题目描述:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如:输入如下所示的一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

解析:

  • 方法一:首先对数组进行排序,在一个有序数组中,次数超过一半的必定是中位数,那么可以直接取出中位数,然后遍历数组,看中位数是否出现次数超过一半,这取决于排序的时间复杂度,最快为O(nlogn)。
  • 遍历数组,用 字典保存每个数出现的次数,这样可以从字典中直接判断是否有超过一半的数字,这种算法的时间复杂度为O(n),但是这个性能提升是用O(n)的空间复杂度换来的。
  • 方法三(最优解法):根据数组特点得到时间复杂度为O(n)的算法。根据数组特点,数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数之和还要多。因此,可以在遍历数组的时候设置两个值:一个是数组中的数result,另一个是出现次数times。当遍历到下一个数字的时候,如果与result相同,则次数加1,不同则次数减一,当次数变为0的时候说明该数字不可能为多数元素,将result设置为下一个数字,次数设为1。这样,当遍历结束后,最后一次设置的result的值可能就是符合要求的值(如果有数字出现次数超过一半,则必为该元素,否则不存在),因此,判断该元素出现次数是否超过一半即可验证应该返回该元素还是返回0。这种思路是对数组进行了两次遍历,复杂度为O(n)。
# 利用字典映射,时间复杂度O(N), 空间复杂度O(N)def find_more_than_half_num_1(arr):    if arr is None: print("输入的参数不合法") return 0    arr_len = len(arr)    num_times_dict = {}    i = 0    while i < arr_len: if arr[i] in num_times_dict.keys():     num_times_dict[arr[i]] += 1 else:     num_times_dict[arr[i]] = 1 i += 1    print(num_times_dict)    border_times = arr_len // 2    for a, b in num_times_dict.items(): if b > border_times:     return a    return 0# 最优解法,时间复杂度为O(N)def find_more_than_half_num_2(arr):    if arr is None: print("输入的参数不合法") return 0    result, times, arr_len = arr[0], 1, len(arr)    i = 1    while i < arr_len: if arr[i] == result:     times += 1     if times > arr_len // 2:  return result else:     times -= 1     if times == 0:  result = arr[i]  times = 1 i += 1    # 假设存在满足条件的值,那么就是上方所求的result存储的值    # "抵消法":最极端的例子[2, 1, 2, 3, 2, 5, 2, 5, 2]    i, times = 0, 0    while i < arr_len: if arr[i] == result:     times += 1 if times > arr_len // 2:     return result i += 1    return 0if __name__ == '__main__':    s = [1, 2, 3, 2, 2, 2, 5, 4, 2]    final_result = find_more_than_half_num_2(s)    print(final_result)

34. 连续子数组的最大和

题目描述:给一个数组,返回它的最大连续子序列的和。

解析:用典型的动态规划思想来求解。用 res[ i ] 表示以第 i 个元素结尾的子数组的最大和,那么有以下递推公式:res[ i ]=max(res[ i-1]+data[ i ],data[ i ])。这个公式的含义是:当以第i-1个数字结尾的子数组中所有数字的和小于0时,把这个负数与第i个数累加,则得到的和比第i个数字本身还要小,所以这种情况下res[ i ]就是第i个数字本身。反之,如果以第i-1个数字结尾的子数组中所有数字的和大于0,则与第i个数字累加就得到以第i个数字结尾的子数组中所有数字的和。

def find_greatest_sum_of_subarray(arr):    if arr is None: print("输入的参数不合法") return -1    arr_len = len(arr)    if arr_len == 1: return arr[0]    max_sum, temp_sum = arr[0], arr[0]    i = 1    # 动态规划的思想,将遍历到的每个元素当作是最大连续和的末尾元素    while i < arr_len: if temp_sum < 0:     temp_sum = arr[i] else:     temp_sum += arr[i] # 等价的操作 # temp_sum += arr[i] # if temp_sum < arr[i]: #     temp_sum = arr[i]  if max_sum < temp_sum:     max_sum = temp_sum i += 1    return max_sumif __name__ == '__main__':    s = [6, -3, -2, 7, -15, 1, 2, 2]    result = find_greatest_sum_of_subarray(s)    print(result)

35. 把数组排成最小的数

题目描述:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

解析:本题实际上需要找到一个排序规则,数组根据这个排序规则进行重排之后可以连成一个最小的数字。要确定这样的排序规则,也就是对于两个数字m和n,通过一个规则确定哪个应排在前面。根据题目要求发现,两个数字m和n能拼接成mn和nm,如果mn<nm,那m应该在前;如果nm<mn,那么n应该在前。因此,我们得到的排序规则如下:

  • 若mn>nm,则m大于n
  • 若mn<nm,则m小于n
  • 若mn=nm,则m等于n
def compare_mn_and_nm(s1, s2):    combination_1 = s1 + s2    combination_2 = s2 + s1    if combination_1 > combination_2: return True    else: return Falsedef new_bubble_sort(str_arr):    length = len(str_arr)    for i in range(length-1, 0, -1): for j in range(i):     if compare_mn_and_nm(str_arr[j], str_arr[j+1]):  str_arr[j], str_arr[j+1] = str_arr[j+1], str_arr[j]def print_min_number(arr):    if arr is None or len(arr) <= 0: print("输入的参数不合法") return None    result = ''    arr_len = len(arr)    str_arr = ['']*arr_len    # 将正整数列表转化成字符串列表    for i in range(arr_len): str_arr[i] = str(arr[i])    # 采用特定的规则去排序    new_bubble_sort(str_arr)    # 顺序组合为最终结果    for i in range(arr_len): result += str_arr[i]    return resultif __name__ == '__main__':    s = [3, 32, 321, 6, 42]    print(print_min_number(s))

36. 数组中的逆序对

题目描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。

解析:采用分治的思想,类比归并排序算法来分析此题。首先将数组分隔成子数组,统计出子数组内部逆序对数目,然后再统计相邻子数组之间的逆序对数目,统计过程中还需要对数组进行排序,这实际上就是归并排序的过程。主要考虑的是合并两个有序序列时,计算逆序对数。对于两个有序升序序列,设置两个下标分别指向开始位置,每次比较两个指针对应的值,如果第一个序列当前值大于第二个序列当前值,则有第一个序列“当前长度”个逆序对。

class Solution:    def __init__(self): self.res = 0    def inverse_pairs(self,arr): if arr is None or len(arr) <= 0:     return 0 self.find_reverse_pairs(arr, 0, len(arr)-1)    def find_reverse_pairs(self, arr, low, high): if low < high:     mid = low + (high-low)//2     self.find_reverse_pairs(arr, low, mid)     self.find_reverse_pairs(arr, mid+1, high)     self.merge(arr, low, mid, high)    def merge(self, arr, low, mid, high): temp = [0]*(high-low+1) k, i, j = 0, low, mid+1 while i <= mid and j <= high:     if arr[i] <= arr[j]:  temp[k] = arr[i]  k += 1  i += 1     else:  temp[k] = arr[j]  k += 1  j += 1  # 从此i位置开始的数据和arr[j]均构成逆序对  self.res += (mid-i+1) while i <= mid:     temp[k] = arr[i]     k += 1     i += 1 while j <= high:     temp[k] = arr[j]     k += 1     j += 1 for i in range(len(temp)):     arr[low+i] = temp[i]if __name__ == '__main__':    s = [7, 5, 6, 4]    test = Solution()    test.inverse_pairs(s)    print(test.res)

37. 数字在排序中出现的次数

题目描述:统计一个数字在排序数组中出现的次数。例如,输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于数字3在该数组中出现了4次,所以函数返回4。

解析:由于数组有序,如果知道了第一个k出现的位置和最后一个k出现的位置,那么我们就可以直接算出有多少个k。因此将思路转化为通过二分查找求第一个和最后一个k出现的位置。以第一个k出现的位置为例,利用二分查找算法可以直接对数组进行二分,而**每次总是拿中间的数字和k做比较,如果中间的数字大于k,那么第一个k只有可能出现在左边,下一次直接在数组左半段继续进行二分查找;如果中间的数字小于k,则第一个k只有可能出现在右边,则在右半段再查找;如果中间的数字等于k,我们先判断它前面的一个数字是不是k,如果不是,那么这个中间的数字就是第一个出现的位置,反之,如果中间数字前面的数字是k,那么第一个k仍然在前半段,继续查找。**同理,找最后一个k出现的位置方法类似,可以使用两个函数分别获得。

def find_num_in_array(arr, num):    if arr is None or len(arr) == 0: print("输入的参数有误") return -1    if num > arr[-1] or num < arr[0]: return 0    first = find_first_index(arr, num)    last = find_last_index(arr, num)    if first == -1 or last == -1: return 0    else: return last-first+1def find_first_index(arr, num):    low, high = 0, len(arr)-1    while low <= high: mid = low + (high-low)//2 if arr[mid] < num:     low = mid + 1 elif arr[mid] > num:     high = mid - 1 else:     mid -= 1     # 满足此条件时,找到num出现的首坐标     if low > mid or arr[mid] != num:  return mid + 1     else:  high = mid    return -1def find_last_index(arr, num):    low, high = 0, len(arr) - 1    while low <= high: mid = low + (high - low) // 2 if arr[mid] < num:     low = mid + 1 elif arr[mid] > num:     high = mid - 1 else:     mid += 1     # 满足此条件时,找到num出现的尾坐标     if high < mid or arr[mid] != num:  return mid - 1     else:  low = mid    return -1if __name__ == '__main__':    test_arr_1 = [1, 2, 3, 3, 3, 3, 4, 5]    test_arr_2 = [1, 2, 3, 4, 5, 7, 8, 10]    times_1 = find_num_in_array(test_arr_1, 3)    times_2 = find_num_in_array(test_arr_2, 6)    print(times_1, times_2)

38. 数组中只出现一次的数字

题目描述:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度为O(n),空间复杂度为O(1)。

解析:看一个比较简单的情况,如果数组中只有一个数字出现一次,其他都出现两次。那么可以想到异或运算。异或运算有一个比较好的性质是:相同为0,相异为1。也就是说,任何一个数字异或它自己都等于0,而0异或任何数都等于那个数。因此,从头到尾依次异或数组中的每个数字,那么最终结果刚好是那个只出现一次的数字,重复的数字在异或过程中被抵消了。这是一种比较巧妙的思路,然而,本题只出现一次的数字有两个,简单的异或无法解决。但是,借助这种思路,可以进一步分析,如果能**把数组分成两个子数组,使每个子数组包含一个只出现一次的数字,而其他数字成对出现,**那么通过上述解法就可以找到两个元素。

具体思路是:首先仍然从前向后依次异或数组中的数字,那么得到的结果是两个只出现一次的数字的异或结果,其他成对出现的数字被抵消了。由于这两个数字不同,所以异或结果肯定不为0,也就是这个异或结果一定至少有一位是1,在结果中找到第一个为1的位的位置,记为第n位。接下来,以第n位是不是1为标准,将数组分为两个子数组,第一个数组中第n位都是1,第二个数组中第n位都是0。这样,便实现了目标。最后,两个子数组分别异或则可以找到只出现一次的数字。

def find_only_nums_in_array(arr):    if arr is None: print("输入的参数不合法") return None    arr_len = len(arr)    result = list()    temp_result, i = 0, 0    while i < arr_len: temp_result ^= arr[i] i += 1    position = 0    while (temp_result >> position) & 1 != 1: position += 1    num_1, num_2 = 0, 0    i = 0    while i < arr_len: if (arr[i] >> position) & 1 == 1:     num_1 ^= arr[i] else:     num_2 ^= arr[i] i += 1    result.append(num_1)    result.append(num_2)    return resultif __name__ == '__main__':    test_arr = [2, 4, 3, 6, 3, 2, 5, 5]    final_result = find_only_nums_in_array(test_arr)    print(final_result)

39. 数组中重复的数字

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

解析:数组重排。由于题目中告诉所有的数字都在0到n-1的范围内,因此如果没有重复,那么所存储的值也正好是0到n-1这n个数字,把原数组重新排列为一个元素和对应下标值相同的数组。具体思路如下:从头到尾扫描整个数组中的数字,当扫描到下标为i的数字时,首先比较这个数字(用m表示)是不是等于下标i,如果是,接着比较下一个数字;如果不是,则将其与第m个数字比较,若与第m个数字相同,则说明它就是一个重复数字,如果不同,就将其与第m个数字进行交换,也就是把它放到自己应在的位置去。重复这个过程,直到该位置上的数与下标相同为止。该算法看起来是两层循环,但是每个数字最多进行两次交换就会找到属于自己的位置,因为总的时间复杂度还是O(n),不需要额外内存。

def find_duplicate_num(arr):    if arr is None or len(arr) <= 0: print("输入的参数有误") return None    arr_len = len(arr)    i = 0    while i < arr_len: # 每经过一个下标,进行调整数组的内容,使得满足arr[i] = i while arr[i] != i:     # 找到重复的数字     if arr[arr[i]] == arr[i]:  return arr[i]     else:  arr[arr[i]], arr[i] = arr[i], arr[arr[i]] i += 1    return -1if __name__ == '__main__':    test_arr = [2, 3, 1, 0, 2, 5, 3]    result = find_duplicate_num(test_arr)    print(result)

补充说明:

(1) 如果只要求判断是否有重复元素,不用找到该值,那么可以使用异或的思路,所有的元素和从0到n-1的下标一起异或,那么如果没有重复元素,相当于从0到n-1每个元素都出现了两次(下标和对于的元素),最后的异或结果一定是0,否则说明有重复元素。

(2) 上述数组重排的思路虽然比较巧妙,但是一个缺点是改变了原来的数组,如果题目要求不能修改原来的数组,一个是可以使用哈希表,另一个是可以使用剑指Offer上给出的二分查找思路,但是相对比较麻烦。具体如下:

以长度为8的数组{2,3,5,4,3,2,6,7}为例,根据题目要求,这个长度为8的数组,所有元素都在1到7的范围内,中间的数字4将1—7分成两部分,分别为1—4和5—7,接下来统计1—4在数组中出现的次数,发现是5次,则说明这4个数字中一定有重复数字。接下来再把1—4分成1、2和3、4两部分,1和2一共出现了两次,3和4一共出现了3次,说明3和4中有一个重复,再分别统计即可得到是3重复了。这并不保证找出所有的重复数字,比如2就没有找到。

实际上,这种二分查找时间复杂度也达到了O(nlogn),不如用哈希表空间换时间来的直观。

40. 构建乘积数组

题目描述:给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1]。其中B中的元素B[i]=A[0] * A[1]... * A[i-1] * A[i+1]... * A[n-1]。不能使用除法。

解析:可以把B[i]=A[0]*A[1]*A[2]*···*A[i-1]*A[i+1]*···*A[n-1]看成是两部分的乘积,第一部分是i之前的所有项,记为C[i],即C[i]=A[0]*A[1]*A[2]*···*A[i-1],第二部分是i之后的所有项,记为D[i],即D[i]=A[i+1]*···*A[n-1]。经过这样的分隔后,数组B就相当于可以用如下的矩阵来构建,B[i]为矩阵中第i行所有元素的乘积。由此,不难得出相应的规律:首先B[i]=C[i]*D[i],而C[i]可以通过自上而下的顺序进行计算,即C[0]=1,C[i]=C[i-1]*A[i-1],同理,D[i]可以通过自下而上的顺序进行计算,即D[len-1]=1,D[i]=D[i+1]*A[i+1]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsLdvCQd-1648472524135)(D:\Typora_files\乘积数组.png)]

def multiply(arr):    if arr is None or len(arr) <= 0: print("输入的参数有误") return None    arr_len = len(arr)    multiply_result = [0]*arr_len    # 分成两部分的乘积, 0~i-1和(i+1)~n    # 第一部分,自上而下    multiply_result[0] = 1    i = 1    while i < arr_len: multiply_result[i] = multiply_result[i-1] * arr[i-1] i += 1     # 第二部分,自下而上    temp = 1    j = arr_len - 2    while j >= 0: temp *= arr[j+1] multiply_result[j] *= temp j -= 1    return multiply_resultif __name__ == '__main__':    test_arr = [1, 2, 3, 4, 5]    result = multiply(test_arr)    print(result)

41. 替换空格

题目描述:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

解析:
  对于这个题目,首先想到原来的一个空格替换为三个字符,字符串长度会增加,因此,存在以下两种不同的情况:(1)允许创建新的字符串来完成替换。(2)不允许创建新的字符串,在原地完成替换。第一种情况比较简单。对于第二种情况,有以下两种解法:

  • 时间复杂度为O(n^2)的解法
    从头到尾遍历字符串,当遇到空格时,后面所有的字符都后移2个。

  • 时间复杂度为O(n)的解法。
    可以先遍历一次字符串,这样可以统计出字符串中空格的总数,由此计算出替换之后字符串的长度,每替换一个空格,长度增加2,即替换之后的字符串长度为原来的长度+2*空格数目。接下来从字符串的尾部开始复制和替换,用两个指针P1和P2分别指向原始字符串和新字符串的末尾,然后向前移动P1,若指向的不是空格,则将其复制到P2位置,P2向前一步;若P1指向的是空格,则P1向前一步,P2之前插入%20,P2向前三步。这样,便可以完成替换,时间复杂度为O(n)。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R4H3gFLf-1648472524135)(D:\Typora_files\替换空格.png)]

# 允许创建新的字符串来完成替换def replace_space_1(s):    if s is None: return None    res = ''    i, len_s = 0, len(s)    while i < len_s: if s[i] == ' ':     res += '%20' else:     res += s[i] i += 1    return res# 不允许创建新的字符串,在原地完成替换def replace_space_2(s):    if s is None: return None    space_num, i = 0, 0    while s[i] != '#': if s[i] == ' ':     space_num += 1 i += 1    if space_num == 0: print("当前字符串中不存在空格") return    p1 = i    p2 = p1 + space_num * 2    if p2 + 1 > len(s): print("字符串数组空间不够,无法完成任务\n") return    # 只要空格没被处理完,p1和p2永远有差值    while p2 > p1: if s[p1] == ' ':     s[p2] = '0'     s[p2-1] = '2'     s[p2-2] = '%'     p2 -= 3     p1 -= 1 else:     s[p2] = s[p1]     p2 -= 1     p1 -= 1    print("替换空格成功")    returnif __name__ == '__main__':    # test_s_1 = 'we are family '    # print(replace_space_1(test_s_1))    # 在这里用'&'表示字符串的结尾    test_s_2 = 'we are family ' + '#' + '*'*20    print(test_s_2)    test_s_2 = list(test_s_2)    replace_space_2(test_s_2)    print(test_s_2)# 【注】在python中要对字符串进行修改,需要先将其转换成列表再进行操作

42. 字符串的排列

题目描述:输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。(输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。)

解析:要求整个字符串的排列,可以看成两步:第一步:求所有可能出现在第一个位置的字符,即把第一个字符与后面的字符依次交换。(按照第一步去排列函数)第二步:固定一个字符,求后面所有字符的排列。很明显,求后面所有字符的排列,仍然可以把所有的字符分成两个部分:后面的字符的第一个字符以及这个字符之后的所有字符,然后把第一个字符逐一与其后的字符交换。因此,这是典型的递归思路。

def swamp(string, i, j):    temp = string[i]    string[i] = string[j]    string[j] = tempdef is_duplication(string, start, now_index):    i = start    while i < now_index: if string[i] == string[now_index]:     return True i += 1    return False# start表示当前字符串的开始的位置,或者说是与其后的每个字符进行交换的位置def permutation(string, start):    if string is None or start < 0: return    # 递归结束条件    if start == len(string) - 1: print(''.join(string)) return    i = start    while i < len(string): # 此举是为了去除重复的组合 if not is_duplication(string, start, i):     swamp(string, start, i)     # 若把下面的递归函数换成输出当前所表示的符串,结果即为每个字符只与字符串的第一个字符进行交换后的结果     permutation(string, start+1)     # 恢复,为下一次交换准备     swamp(string, start, i) i += 1    returnif __name__ == '__main__':    s = 'aaa'    list_s = list(s)    permutation(list_s, 0)

43. 第一次只出现一次的字符

题目描述:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)。

解析:以下三种解法:(1)用 HashMap 建立每个字符与其出现次数的映射,然后再依次遍历字符串时,找到第一个出现次数为1的字符,返回其位置即可。(2)更进一步,因为该字符串全部是字母,所以可以用一个数组代替哈希表,数组下标就代表该字母。(3)使用python内置函数count进行统计和判断。

# 使用字典映射字符出现的次数def find_first_not_repeat_char(string):    if string is None: return -1    char_map = dict()    len_s = len(string)    print(string)    i = 0    while i < len_s: if string[i] in char_map.keys():     char_map[string[i]] += 1 else:     char_map[string[i]] = 1 i += 1    i = 0    while i < len_s: if char_map[string[i]] == 1:     return i i += 1    return -1# 也可用数组代替字典,因为字符串中全为字母,可将对应的ASCII码映射到数组的下标上def find_first_not_repeat_char_1(string):    if string is None: return -1    # A-Z对应的ASCII码为65-90,a-z对应的ASCII码值为97-122    char_arr = [0]*58 # 122-65+1    len_s = len(string)    i = 0    while i < len_s: char_arr[ord(string[i])-65] += 1 i += 1    i = 0    while i < len_s: if char_arr[ord(string[i])-65] == 1:     return i i += 1    return -1# 使用系统的count函数实现def find_unique_char(string):    if string is None: return -1    len_s = len(string)    i = 0    while i < len_s: if string.count(string[i]) == 1:     return i i += 1    return -1if __name__ == '__main__':    s = "abaa"    # print(find_unique_char(s))    print(find_first_not_repeat_char(s))    print(find_first_not_repeat_char_1(s))

44. 左旋转字符串

题目描述:汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。

解析:字符串左移k位,就相当于将字符串分为两部分,第一部分是前k位,另一部分是剩余的其他位,然后将这两部分交换顺序即可得到最后结果。因此,我们可以得到以下的三次反转算法:将字符串分为两部分,即前k个字符和剩余的其他字符,然后分别对这两部分进行反转,然后再对整个字符串进行一次反转,这样得到的结果就是我们想要的循环左移之后的字符串。事实上,这并不难理解,前后两部分各自经历了两次反转,因此每一部分的顺序并没有改变,只是将前后两部分进行了交换。对字符串进行一次反转,需要一次扫描,因此次算法时间复杂度为O(n)。

def reverse_string(string, start, end):    while start < end: string[start], string[end] = string[end], string[start] start += 1 end -= 1def left_rotate_string(string, num):    if string is None or num < 0 or num > len(string): print("输入的参数不合法") return string    if num == 0: print("原字符串即为左旋转后的结果") return string    temp_string = list(string)    reverse_string(temp_string, 0, num-1)    reverse_string(temp_string, num, len(temp_string)-1)    reverse_string(temp_string, 0, len(temp_string)-1)    return ''.join(temp_string)if __name__ == '__main__':    test_string = 'abcXYZdef'    print(left_rotate_string(test_string, 3))

45. 反转单词序列

题目描述:牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是**“I am a student.”**。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

解析:第一步:反转整个序列中所有的字符,这时会发现不但反转了单词的顺序,单词中的字母顺序也被反转,因此需要第二步的调整。第二步:以空格为分隔,依次反转每个单词,即让每个单词会到原来的正常顺序。

def reverse_str(string, start, end):    while start < end: string[start], string[end] = string[end], string[start] start += 1 end -= 1def reverse_sentence(string):    if string is None: print("输入的参数有误") return string    string_list = list(string)    len_s = len(string)    reverse_str(string_list, 0, len_s-1)    begin, i = 0, 0    while i < len_s: if string_list[i] == ' ':     reverse_str(string_list, begin, i - 1)     begin = i + 1 i += 1    reverse_str(string_list, begin, len_s-1) return ''.join(string_list)if __name__ == '__main__':    test_string = 'I am a student'    reverse_sentence(test_string)

46. 字符串转换成整数

题目描述:将一个字符串转换成一个整数的功能,但是string不符合数字要求时返回0,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。

解析:主要需要注意的点有以下几个:

  • 字符串是否为null或者字符串是否为空字符串。
  • 字符串对于正负号的处理,特别是正号,可能有也可能没有,但都代表正数
  • 输入值是否合法,判断除首位可能为正负号外,其他位是否都是数字
  • int为32位,最大的整数是刚刚超过21亿,也就是10位十进制数
  • 使用错误标志,区分合法值0和非法值0
def str_to_int(string):    if string is None or len(string) < 1: print("输入的参数有误") return 0, False    first_char = string[0]    if first_char == '-': flag = -1    elif first_char == '+': flag = 1    elif '0' <= first_char <= '9': flag = 1 # 修改字符串前缀,方便统一处理 string = '+' + string    else: return 0, False    len_s, i = len(string), 1    res = 0    while i < len_s: if string[i] < '0' or string[i] > '9':     # 想处理关于带有小数的字符串     if string[i] == '.' and i != 1:  for j in range(i+1, len_s):      if string[j] > '9' or string[j] < '0':   return 0, False  break     else:  return 0, False res = res*10 + ord(string[i]) - 48 i += 1    if flag == 1: return res, True    else: return -1*res, Trueif __name__ == '__main__':    test_string = '-323.15935'    result, is_valid = str_to_int(test_string)    if is_valid: print("字符串转换为整数的结果为:", result)    else: print("当前输入的字符串无效,无法进行整数转换")

47. 字符流中第一个不重复的字符

题目描述:请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

解析:用数组代替哈希表,将字符的ASCLL码作为数组下标,字符对应出现的次数作为数组的元素进行保存。

def find_first_not_dup_char(string):    if string is None or len(string) <= 0: print("输入的参数有误") return "#"    len_s = len(string)    # 目前ASCII码字符总数为256个    hash_arr = [0]*256    for i in range(len_s): hash_arr[ord(string[i])] += 1    for i in range(len_s): if hash_arr[ord(string[i])] == 1:     print("找到第一个不重复的字符")     return string[i]    print("不存在不重复的字符")    return '#'if __name__ == '__main__':    string_flow = "(g)o(ogle"    result = find_first_not_dup_char(string_flow)    print(result)

48. 正则表达式匹配

题目描述:请实现一个函数用来匹配包括’.‘和’*‘的正则表达式。模式中的字符**’.表示任意一个字符**,而**’*’**表示它前面的字符可以出现任意次(包含0次,1次和多次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a“和”ab*ac*a“匹配,但是与”aa.a“和”ab*a"均不匹配。

解析:

  • 当模式串的第二个字符不是*,问题比较简单:若字符串的第一个字符和模式串的第一个字符匹配时,字符串和模式串指针都向后移动一个字符,然后匹配剩余的字符串和模式。如果第一个字符不匹配,那么就可以直接返回false。
  • 当模式串的第二个字符是*,情况就比较复杂,因为可能有多种不同的匹配方式:
    • 无论第一个字符是否相等,模式串向后移动两个字符,相当于*和它前面的字符被忽略,因为*可以代表前面的字符出现0次。
    • 如果模式串第一个字符和字符串第一个字符匹配,则字符串向后移动一个字符,比较下一位,而模式串此时有两种情况:模式串向后移动两个字符,也可以保持模式不变(因为*可以代表前面的字符出现多次)。
def match_recursion(string, i, pattern, j):    if i == len(string) and j == len(pattern): # 表示匹配完成 return True    if i == len(string) and j < len(pattern): return False    if i < len(string) and j == len(pattern): return False    # 能进入到下面的代码中去的条件必是:i < len(string) and j < len(pattern)    # 如果模式串的第二个字符为'*'    if j+1 < len(pattern) and pattern[j+1] == '*': # 若当前字符匹配 if string[i] == pattern[j] or pattern[j] == '.':     # 分别表示匹配0个,1个和多个     return match_recursion(string, i, pattern, j+2) or match_recursion(string, i+1, pattern, j+2) \     or match_recursion(string, i+1, pattern, j) # 若当前字符不匹配,进入0匹配 else:     return match_recursion(string, i, pattern, j+2)    # 若模式串的第二个字符不为'*',或是j+1 == len(pattern)    else: # 若当前指针对应的字符匹配,两指针同时向后移动一个单位 if string[i] == pattern[j] or pattern[j] == '.':     return match_recursion(string, i+1, pattern, j+1) # 当前两指针对应的字符不匹配 else:     return Falsedef match(string, pattern):    if string is None or pattern is None: return False    return match_recursion(string, 0, pattern, 0)if __name__ == '__main__':    result = match('aaa', '.*')    print('字符串的匹配结果为:', result)

49. 表示数值的字符串

题目描述:请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100", “5e2”, “-123”," 3.1416" 和 “-1E-16” 都表示数值。 但是 “12e”, “1a3.14”, “1.2.3”, “±5” 和 “12e+4.3” 都不是。

解析:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XuNP2f8E-1648472524137)(D:\Typora_files\数值的表示.png)]

A为数值的整数部分,B为跟在小数点之后的小数部分,C为跟在e或者E之后的指数部分。其中,A部分可以没有,比如小数.123代表0.123。如果一个数没有整数部分,那么小数部分必须有。具体说来,A和C(也就是整数部分和指数部分)都是可能以"+"、"-"开头或者没有符号的数字串,B是数字序列,但前面不能有符号。

import re# 应用正则表达式去判断字符串def is_number_string(string):    if string is None or len(string) <= 0: print("输入的参数有误") return False    # '+', '.'需要转义,'-'不需要转义    # result = re.fullmatch(r'[\+-]?[0-9]*(\.[0-9]*)?([eE][\+-]?[0-9]+)?', string)    # 00000e10、.000e3都应该被视为不匹配,1.000001e+10、10000e9为正常的    pattern = r'[\+-]?[1-9]+[0-9]*(\.[0-9]*)?([eE][\+-]?[0-9]+)?|\.[0-9]*[1-9]+[0-9]*([eE][\+-]?[0-9]+)?'    result = re.fullmatch(pattern, string)    # re.fullmatch()和re.match()的区别    # match尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match() 就返回 none。    # fullmatch要求对整个字符串进行匹配才算成功,否则返回none    if result: return True    else: return False# 逐位判断def is_number_string_1(string):    if string is None or len(string) <= 0: print("输入的参数有误") return False    sign, decimal, has_e = False, False, False    i, len_s = 0, len(string)    while i < len_s: if string[i] == 'E' or string[i] == 'e':     if i == len_s-1:  # 字符串的最后一位不可以为e/E  return False     if has_e:  # 字符串中不能出现两次e/E  return False     has_e = True elif string[i] == '.':     if decimal or has_e:  # 字符串中不可以出现两次小数点,指数之后不能有小数点  return False     decimal = True elif string[i] == '+' or string[i] == '-':     # 第一次出现'+'/'-', 不在开头也不在e/E之后     if not sign and i != 0 and string[i-1] != 'e' and string[i-1] != 'E':  return False     # 第二次出现'+'/'-', 不在e/E之后     if sign and string[i-1] != 'e' and string[i-1] != 'E':  return False     sign = True elif string[i] > '9' or string[i] < '0':     return False i += 1    return Trueif __name__ == '__main__':    test_string = '.00000e+100'    if is_number_string(test_string): print(test_string, ':为数值字符串')    else: print(test_string, ':不是数值字符串')

50. 重建二叉树

题目描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回根结点。

解析:树的遍历有三种:分别是前序遍历、中序遍历、后序遍历。本题是根据前序和中序遍历序列重建二叉树,可以通过一个具体的实例来发现规律,不难发现:前序遍历序列的第一个数字就是树的根结点。在中序遍历序列中,可以扫描找到根结点的值,则左子树的结点都位于根结点的左边,右子树的结点都位于根结点的右边。这样,就通过这两个序列找到了树的根结点、左子树结点和右子树结点,接下来左右子树的构建可以进一步通过递归来实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZAFQFiVV-1648472524138)(D:\Typora_files\重建二叉树.png)]

from common import BiTNodedef reconstruct_binary_tree_(pre, middle, pre_begin, pre_end, middle_begin, middle_end):    # pre:4, 7    middle:4, 7,比如若此时'4'结点已经重建完成,需要对它进行左子树的递归,进入后发现并无左子树    # 递归结束的条件①,基于某结点的递归,进入函数后,发现无左子树或者右子树    if pre_begin > pre_end or middle_begin > middle_end: return None    root = BiTNode()    root.data = pre[pre_begin]    # 递归提前结束的条件,当前的所划定的范围中仅仅只有一个结点,不需要进行左右子树递归    # pre:4, 7    middle:4, 7如:当'4'结点已经重建完成,需要对它进行右子树的递归,发现仅剩'7'一个结点,此时生成'7'结点后    # 就不在需要进行左右子树的递归了,它已是唯一的结点    if pre_begin == pre_end or middle_begin == middle_end: return root    root_in_middle_index = middle_begin    while root_in_middle_index <= middle_end and middle[root_in_middle_index] != pre[pre_begin]: root_in_middle_index += 1    left_count = root_in_middle_index - middle_begin    root.lchild = reconstruct_binary_tree_(pre, middle, pre_begin+1, pre_begin+left_count, middle_begin, root_in_middle_index-1)    root.rchild = reconstruct_binary_tree_(pre, middle, pre_begin+left_count+1, pre_end, root_in_middle_index+1, middle_end)    return rootdef reconstruct_binary_tree(pre, middle):    if pre is None or middle is None: return None    return reconstruct_binary_tree_(pre, middle, 0, len(pre)-1, 0, len(middle)-1)if __name__ == '__main__':    s1 = [1, 2, 4, 7, 3, 5, 6, 8]    s2 = [4, 7, 2, 1, 5, 3, 8, 6]    result = reconstruct_binary_tree(s1, s2)

51. 树的子结构

题目描述:输入两棵二叉树A,B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

解析:要查找树A中是否存在和树B结构一样的子树,可以分为两步:**第一步,在树A中找到和树B的根结点值一样的结点R;第二步,判断树A中以R为根结点的子树是不是包含和树B一样的结构。**对于这两步,第一步实际上就是树的遍历,第二步是判断是否有相同的结构,这两步都可以通过递归来实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WaRWWDGr-1648472524139)(D:\Typora_files\树的子结构.png)]

from common import BiTNodedef has_sub_tree(root1, root2):    if root1 is None or root2 is None: return False    res = False    if root1.data == root2.data: # 以当前结点为根结点,进行第二步操作 res = if_root1_has_root2(root1, root2)    if not res: # 对主树的左子树进行查找相同的头结点 res = has_sub_tree(root1.lchild, root2)    if not res: # 对主树的右子树进行查找相同的头结点 res = has_sub_tree(root1.rchild, root2)    return resdef if_root1_has_root2(root1, root2):    # 子树结点为空表示此时它的某分支遍历结束,为True    if root2 is None: return True    # 子树结点不为空而主树结点为空,不匹配    if root1 is None: return False    # 子树与主树的结点不相等,直接不匹配    if root1.data != root2.data: return False    return if_root1_has_root2(root1.lchild, root2.lchild) and if_root1_has_root2(root1.rchild, root2.rchild)if __name__ == '__main__':    test_root1 = BiTNode()    test_root1.data = 8    node1 = BiTNode()    node1.data = 8    node2 = BiTNode()    node2.data = 7    node3 = BiTNode()    node3.data = 9    node4 = BiTNode()    node4.data = 2    node5 = BiTNode()    node5.data = 4    node6 = BiTNode()    node6.data = 7    test_root1.lchild = node1    test_root1.rchild = node2    node1.lchild = node3    node1.rchild = node4    node4.lchild = node5    node4.rchild = node6    test_root2 = BiTNode()    test_root2.data = 8    node7 = BiTNode()    node7.data = 9    node8 = BiTNode()    node8.data = 2    test_root2.lchild = node7    test_root2.rchild = node8    result = has_sub_tree(test_root1, test_root2)    print(result)

52. 二叉树的镜像

题目描述:操作给定的二叉树,将其变换为原二叉树的镜像。

解析:求一棵树的镜像的过程:先前序遍历这棵树的每个结点,如果遍历到的结点有子结点,就交换它的两个子结点。当交换完所有的非叶结点的左、右子结点后,就可以得到该树的镜像。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jeMmMtcc-1648472524139)(D:\Typora_files\镜像二叉树.png)]

from common import BiTNodedef mirror(root):    if root is not None: if root.lchild is not None or root.rchild is not None:     temp = root.lchild     root.lchild = root.rchild     root.rchild = temp     mirror(root.lchild)     mirror(root.rchild)if __name__ == '__main__':    test_root = BiTNode()    test_root.data = 8    node1 = BiTNode()    node1.data = 6    node2 = BiTNode()    node2.data = 10    node3 = BiTNode()    node3.data = 5    node4 = BiTNode()    node4.data = 7    node5 = BiTNode()    node5.data = 9    node6 = BiTNode()    node6.data = 11    test_root.lchild = node1    test_root.rchild = node2    node1.lchild = node3    node1.rchild = node4    node2.lchild = node5    node2.rchild = node6    mirror(test_root)    print(test_root.lchild.data)

53. 从上到下打印二叉树

题目描述:从上往下打印出二叉树的每个节点,同层节点从左至右打印。

解析:本题实际上就是二叉树的层次遍历,深度遍历可以用递归或者栈,而层次遍历很明显应该使用队列。每次打印一个结点时,如果该结点有子结点,则将子结点放到队列的末尾,接下来取出队列的头重复前面的打印动作,直到队列中所有的结点都打印完毕。

from common import BiTNodefrom queue import Queuedef print_tree_up_to_low(root):    if root is None: print("输入的参数有误") return    # 创建队列    node_queue = Queue()    # 根结点进入队列    node_queue.put(root)    # 队列不为空时,一直打印    while not node_queue.empty(): temp = node_queue.get() print(temp.data, '-->', end='', sep='') # 查看当前出队列的结点是否存在左右子结点,若存在则顺序入队列 if temp.lchild is not None:     node_queue.put(temp.lchild) if temp.rchild is not None:     node_queue.put(temp.rchild)    returnif __name__ == '__main__':    test_root = BiTNode()    test_root.data = 8    node1 = BiTNode()    node1.data = 6    node2 = BiTNode()    node2.data = 10    node3 = BiTNode()    node3.data = 5    node4 = BiTNode()    node4.data = 7    node5 = BiTNode()    node5.data = 9    node6 = BiTNode()    node6.data = 11    test_root.lchild = node1    test_root.rchild = node2    node1.lchild = node3    node1.rchild = node4    node2.lchild = node5    node2.rchild = node6    print_tree_up_to_low(test_root)

54. 二叉搜索树的后序遍历序列

题目描述:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

解析:对于后续遍历序列,序列的最后一个值一定是树的根结点,而由二叉搜索树的性质:左小右大,可以从头开始遍历,当遍历到某个值比根结点大时停止,记为flag,此时flag之前的所有数值都是二叉搜索树的左子树的结点,flag以及flag之后的所有数都是二叉搜索树的右子树的结点。这是由二叉搜索树以及后序遍历共同决定的。接下来,就可以把任务交给递归,同样的方法去判断左子树和右子树是否是二叉搜索树,这显然是典型的递归解法。

举例:以{5,7,6,9,11,10,8}为例,后序遍历结果的最后一个数字8就是根结点的值。在这个数组中,前3个数字5、7和6都比8小,是值为8的结点的左子树结点;后3个数字9、11和10都比8大,是值为8的结点的右子树结点。接下来用同样的方法确定与数组每一部分对应的子树的结构。这其实就是一个递归的过程。对于序列5、7、6,最后一个数字6是左子树的根结点的值。数字5比6小,是值为6的结点的左子结点,而7则是它的右子结点。同样,在序列9、11、10中,最后一个数字10是右子树的根结点,数字9比10小,是值为10的结点的左子结点,而11则是它的右子结点,所以它对应的二叉搜索树如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lRr7xKxp-1648472524140)(D:\Typora_files\二叉搜索树的后序遍历序列.png)]

def verify_sequence_of_bst(sequence):    if sequence is None or len(sequence) <= 0: print("输入的参数不合法") return False    return verify_sequence_of_bst_(sequence, 0, len(sequence)-1)def verify_sequence_of_bst_(sequence, start, end):    if start >= end: return True    # 找到第一个大于end索引处值的位置    i = start    while i < end: if sequence[i] > sequence[end]:     break i += 1    # 判断从i到end之间的值是否都满足大于arr[end]    j = i + 1    while j < end: if sequence[j] < sequence[end]:     return False j += 1    # 递归判断所划分出来的左子树和右子树    return verify_sequence_of_bst_(sequence, start, i-1) and verify_sequence_of_bst_(sequence, i, end-1)if __name__ == '__main__':    # 首先保证输入的内容是不重复的    test_sequence= [5, 7, 6, 9, 11, 10, 8]    if verify_sequence_of_bst(test_sequence): print('YES')    else: print('NO')

55. 二叉树中和为某一值的路径

题目描述:输入一颗二叉树的根结点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的列表中,数组长度大的数组靠前)

解析:本题实质上就是深度优先搜索。使用前序遍历的方式对整棵树进行遍历,当访问到某一个结点时,将该结点添加到路径上,并且累加该结点的值。当访问到的结点是叶结点时,如果路径中的结点值之和正好等于输入的整数,则说明是一条符合要求的路径。如果当前结点不是叶结点,则继续访问它的子结点。当找到一条符合要求的路径之后,需要回溯进一步查找别的路径,因此,这实际上仍然是一个递归的过程,特别注意在函数返回之前要删掉当前结点,从而才可以正确的回溯。

from common import BiTNodeimport copyclass Solution:    def __init__(self): self.result = [] self.sum = 0    def find_path(self, root, target): if root is None:     return temp = [] self.find_path_(root, target, temp) self.sort_result() return    def find_path_(self, root, target, temp): temp.append(root.data) self.sum += root.data # 判断是否为叶子结点,进行前序遍历 if root.lchild is None and root.rchild is None:     if self.sum == target:  # 深拷贝,(理解深浅拷贝的不同,这里使用浅拷贝也是OK的)  copy_temp = copy.deepcopy(temp)  self.result.append(copy_temp) else:     if root.lchild is not None:  self.find_path_(root.lchild, target, temp)     if root.rchild is not None:  self.find_path_(root.rchild, target, temp) # 进行回溯 if len(temp) != 0:     self.sum -= root.data     temp.pop(-1)    # 对得到的结果序列,按长度的大小重新排序(冒泡排序)    def sort_result(self): if len(self.result) == 0:     return len_result = len(self.result) for i in range(len_result-1, 0, -1):     for j in range(i):  if len(self.result[j]) > len(self.result[j+1]):      self.result[i], self.result[j] = self.result[j], self.result[i]if __name__ == '__main__':    test_root = BiTNode()    test_root.data = 10    node1 = BiTNode()    node1.data = 5    node2 = BiTNode()    node2.data = 12    node3 = BiTNode()    node3.data = 4    node4 = BiTNode()    node4.data = 7    test_root.lchild = node1    test_root.rchild = node2    node1.lchild = node3    node1.rchild = node4    test = Solution()    test.find_path(test_root, 22)    print(test.result)

56. 二叉树与双向链表

题目描述:输入一棵二叉搜索树,将二叉搜索树转换成排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

解析:在双向链表中,每个结点都有前后两个指针;二叉树中,每个结点都有两个指向子结点的左右指针,同时,二叉搜索树树也是一种排序的数据结构。因此,从结构上看,双向链表的前后指针和二叉搜索树的左右指针结构相似,因此,可以实现互相之间的转换。首先,根据二叉搜索树的特点,左结点的值<根结点的值<右结点的值,不难发现,使用二叉树的中序遍历得到的数据序列就是递增的排序顺序。因此,确定应该采用中序遍历方法。左右子树具有和原问题相同的结构,因此直接利用递归即可实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lwloJFzP-1648472524140)(D:\Typora_files\二叉搜索树与双向链表_1.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3VQQjItz-1648472524141)(D:\Typora_files\二叉搜索树与双向链表_2.png)]

from common import BiTNodedef convert(tree_root):    if tree_root is None: return None    # cur_end_of_link保存的是当前已经排好的链表的最后一个节点    cur_end_of_link = None    cur_end_of_link = convert_(tree_root, cur_end_of_link)    # 找到双向链表的起始位置    while tree_root.lchild is not None: tree_root = tree_root.lchild    return tree_root, cur_end_of_link# 函数返回的是当前已构建好的双向链表的尾结点指针(初始状态下尾结点指针为None)def convert_(tree_root, cur_end_of_link):    # if tree_root is None:    #     print("我来这里了,哈哈哈")    #     return None    # 进行中序遍历    if tree_root.lchild is not None: cur_end_of_link = convert_(tree_root.lchild, cur_end_of_link)    # 核心转换内容(①当前结点的左指针指向尾结点;②尾结点的右指针(若存在)指向当前结点;③尾结点切换)    tree_root.lchild = cur_end_of_link    if cur_end_of_link is not None: cur_end_of_link.rchild = tree_root    cur_end_of_link = tree_root    if tree_root.rchild is not None: cur_end_of_link = convert_(tree_root.rchild, cur_end_of_link)    return cur_end_of_linkif __name__ == '__main__':    test_root = BiTNode()    test_root.data = 2    node1 = BiTNode()    node2 = BiTNode()    node1.data = 1    node2.data = 3    test_root.lchild = node1    test_root.rchild = node2    head, end = convert(test_root)    print(head.rchild.data)    print(end.data)

57. 二叉树的深度

题目描述:输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

解析:如果一棵树只有一个结点,那么它的深度为1。如果根结点只有左子树而没有右子树,那么树的深度为其左子树深度加1;相反,如果根结点只有右子树而没有左子树,那么深度为右子树深度加1;如果既有左子树又有右子树,那么该树的深度为左、右子树深度的较大值加1。

from common import BiTNodedef tree_depth(root):    if root is None: return 0    left_depth = tree_depth(root.lchild)    right_depth = tree_depth(root.rchild)    depth = left_depth + 1 if left_depth > right_depth else right_depth+1    return depthif __name__ == '__main__':    test_root = BiTNode()    test_root.data = 2    tree_depth_num = tree_depth(test_root)    print(tree_depth_num)

58. 平衡二叉树

题目描述:输入一棵二叉树,判断该二叉树是否是平衡二叉树。这里的定义是:如果某二叉树中任意结点的左、右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

解析:

​ 根据求二叉树深度的思路我们很容易想到一种解法,即:在遍历树的每一个结点时,求其左右子树的深度,判断深度之差,如果每个结点的左右子树深度相差都不超过1,那么就是一棵平衡二叉树。本思路直观简洁,但是需有很多结点需要重复遍历多次,时间效率不高。

​ 一种每个结点只遍历一次的解法。思路如下:采用后序遍历的方式遍历二叉树的每个结点,这样在遍历到每个结点的时候就已经访问了它的左右子树。所以,只要在遍历每个结点的时候记录它的深度,就可以一边遍历一边判断每个结点是不是平衡的。

from common import BiTNodedef is_balance_tree(root):    if root is None: return True    res = get_depth(root)    if res == -1: return False    return Truedef get_depth(root):    if root is None: return 0    # 使用后序遍历,自底向上判断每个子树是否为平衡二叉树    left = get_depth(root.lchild)    if left == -1: return -1    right = get_depth(root.rchild)    if right == -1: return -1    if abs(left-right) > 1: return -1    return left+1 if left > right else right+1if __name__ == '__main__':    test_root = BiTNode()    test_root.data = 2    node1 = BiTNode()    node2 = BiTNode()    node1.data = 1    node2.data = 3    test_root.lchild = node1    node1.lchild = node2    print(is_balance_tree(test_root))

59. 二叉树的下一个结点

题目描述:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

解析:

找一个结点在中序遍历序列中的下一个结点共有三种不同的情况

  • 如果一个结点有右子树,那么它的下一个结点就是它的右子树中最左边的那个结点,也就是说,从它的右子结点出发一直访问左指针,最后就可以找到这个最左结点。

  • 如果一个结点没有右子树,那么需要再进一步考虑,不难知道:如果这个结点是其父结点的左结点,那么根据中序遍历规则,它的下一个结点就是它的父结点。

  • 第三种情况略复杂一些,当一个结点既没有右子树,也不是其父结点的左结点时,可以沿着指向父结点的指针一直向上遍历,直到找到一个结点是其父结点的左结点,如果这样的结点存在,那么这个结点的父结点就是要找的下一个结点。(拐角处)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KUb1z5Kj-1648472524142)(D:\Typora_files\二叉树的下一个结点.jpg)]

def get_next_node(node):    if node is None: return None    if node.rchild is not None: res = node.rchild while res.lchild is not None:     res = res.lchild return res    father = node.parent if father is None: return None    elif father.lchild == node: return father    son = node    while father is not None and father.rchild == son: son = father father = father.parent    return father

60. 对称的二叉树

题目描述:请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

解析:

  • 递归法:对称二叉树满足----根结点的左右子树相同,左子树的左子树和右子树的右子树相同,左子树的右子树和右子树的左子树相同即可。(时间复杂度O(n),空间复杂度O(n))
  • 迭代法(广度优先遍历):广度优先遍历的一般做法是借助队列,这里可以在初始化时把根节点入队两次。每次提取两个结点并比较它们的值(队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像),然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时,或者检测到树不对称(即从队列中取出两个不相等的连续结点)时,该算法结束。(时间复杂度O(n),空间复杂度O(n))
from common import BiTNodefrom queue import Queue# 递归法实现对称判断def is_symmetrical(root):    if root is None: return True    return is_symmetrical_(root.lchild, root.rchild)def is_symmetrical_(p_left, p_right):    if p_left is None and p_right is None: return True    if p_left is None or p_right is None: return False    if p_left.data != p_right.data: return False    # 左子树的左子树和右子树的右子树,左子树的右子树和右子树的左子树,两者同时相等,二叉树才是对称的    return is_symmetrical_(p_left.lchild, p_right.rchild) and is_symmetrical_(p_left.rchild, p_right.lchild)# 广度优先遍历实现对称判断def is_symmetrical_queue(root):    if root is None: return True    node_queue = Queue()    # 这里不重复放root结点两次,而是直接放入根的左右子结点,可显著减少循环次数    node_queue.put(root.lchild)    node_queue.put(root.rchild)    while not node_queue.empty(): t1 = node_queue.get() t2 = node_queue.get() if t1 is None and t2 is None:     continue if t1 is None or t2 is None:     return False if t1.data != t2.data:     return False node_queue.put(t1.lchild) node_queue.put(t2.rchild) node_queue.put(t1.rchild) node_queue.put(t2.lchild)    return Trueif __name__ == '__main__':    test_root = BiTNode()    test_root.data = 8    node1 = BiTNode()    node1.data = 6    node2 = BiTNode()    node2.data = 6    node3 = BiTNode()    node3.data = 5    node4 = BiTNode()    node4.data = 7    node5 = BiTNode()    node5.data = 7    node6 = BiTNode()    node6.data = 5    test_root.lchild = node1    test_root.rchild = node2    node1.lchild = node3    node1.rchild = node4    node2.lchild = node5    node2.rchild = node6    print(is_symmetrical_queue(test_root))

61. 按之字形顺序打印二叉树

题目描述:请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

解析:层次遍历都是借助一定的数据容器来实现的,比如按行打印使用的是队列。在本题,使用的是栈,具体分析如下:可以设置两个辅助栈,在打印某一层的结点时,将下一层的子结点保存到相应的栈里;如果当前打印的是奇数层(第一层、第三层等),则先保存左子节点再保存右子结点到第一个栈中,如果当前打印的是偶数层(第二层、第四层等),则先保存右子结点再保存左子结点到第二个栈中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0SAhsZzi-1648472524143)(D:\Typora_files\之字形打印二叉树_1.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3r4buwVd-1648472524144)(D:\Typora_files\之字形打印二叉树_2.png)]

from collections import dequefrom common import BiTNode# 双端队列的做法反而变得复杂化了# def print_zhi_order_of_tree_1(root):#     if root is None:#  return##     t1 = deque()  # 奇数层双端队列#     t2 = deque()  # 偶数层双端队列##     layer = 1#     t1.append(root)#     while len(t1) != 0 or len(t2) != 0:#  flag = layer % 2#  if flag == 1:  # 打印奇数层#      while len(t1) != 0:#   temp = t1.popleft()#   print(temp.data, end='', sep='-->')#   if temp.lchild is not None:#t2.append(temp.lchild)#   if temp.rchild is not None:#t2.append(temp.rchild)#  else:  # 打印偶数层#      while len(t2) != 0:#   temp = t2.pop()#   print(temp.data, end='', sep='-->')#   if temp.rchild is not None:#t1.appendleft(temp.rchild)#   if temp.lchild is not None:#t1.appendleft(temp.lchild)#  layer += 1#     return# 用列表代替栈的操作def print_zhi_order_of_tree(root):    if root is None: return    t1 = list()    t2 = list()    layer = 1    t1.append(root)    while len(t1) != 0 or len(t2) != 0: flag = layer % 2 if flag == 1:  # 打印奇数层     while len(t1) != 0:  temp = t1.pop()  print(temp.data, end='', sep='-->')  if temp.lchild is not None:      t2.append(temp.lchild)  if temp.rchild is not None:      t2.append(temp.rchild) else:  # 打印偶数层     while len(t2) != 0:  temp = t2.pop()  print(temp.data, end='', sep='-->')  if temp.rchild is not None:      t1.append(temp.rchild)  if temp.lchild is not None:      t1.append(temp.lchild) layer += 1    returnif __name__ == '__main__':    test_root = BiTNode()    test_root.data = 8    node1 = BiTNode()    node1.data = 6    node2 = BiTNode()    node2.data = 4    node3 = BiTNode()    node3.data = 5    node4 = BiTNode()    node4.data = 7    node5 = BiTNode()    node5.data = 9    node6 = BiTNode()    node6.data = 0    test_root.lchild = node1    test_root.rchild = node2    node1.lchild = node3    node1.rchild = node4    node2.lchild = node5    node2.rchild = node6    print_zhi_order_of_tree(test_root)

62. 把二叉树打印成多行

题目描述:从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

解析:在二叉树层次遍历上,使用的是队列,借助队列先进先出的性质实现,具体规律:每次打印一个结点时,如果该结点有子结点,则将子结点放到队列的末尾,接下来取出队列的头重复前面的打印动作,直到队列中所有的结点都打印完毕。在此基础上考虑这里的分行要求,**只要增加两个变量即可:一个用于保存当前层中还没有打印的结点个数,另一个用于记录下一层结点的数目。**而使用队列的话,实际上这两个变量可以统一用队列的长度来实现。

from queue import Queuefrom common import BiTNodedef print_tree_row(root):    if root is None: return    node_queue = Queue()    node_queue.put(root)    # 两变量分别表示,当前打印层的剩余节点数和下一层已入队列的结点数    now_num, after_num = 1, 0    while not node_queue.empty(): temp = node_queue.get() now_num -= 1 if temp.lchild is not None:     after_num += 1     node_queue.put(temp.lchild) if temp.rchild is not None:     after_num += 1     node_queue.put(temp.rchild) if now_num == 0:     print(temp.data)     # 进行交换和重置记录变量     now_num = after_num     after_num = 0 else:     print(temp.data, '-->', sep='',  end='')    returnif __name__ == '__main__':    test_root = BiTNode()    test_root.data = 8    node1 = BiTNode()    node1.data = 6    node2 = BiTNode()    node2.data = 4    node3 = BiTNode()    node3.data = 5    node4 = BiTNode()    node4.data = 7    node5 = BiTNode()    node5.data = 9    node6 = BiTNode()    node6.data = 0    test_root.lchild = node1    test_root.rchild = node2    node1.lchild = node3    node1.rchild = node4    node2.lchild = node5    node2.rchild = node6    print_tree_row(test_root)
// 利用队列的长度,来判断以进行分层import java.util.*;public class TreeNode {    int val = 0;    TreeNode left = null;    TreeNode right = null;    public TreeNode(int val) { this.val = val;    }}public class Solution {    ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) { //思路:使用队列实现 ArrayList<ArrayList<Integer>> res=new ArrayList<>(); if(pRoot==null)     return res; Queue<TreeNode> queue = new LinkedList<>(); //借助队列实现 TreeNode root=pRoot; queue.add(root); while(!queue.isEmpty()){ //队列不空     //当前队列长度代表当前这一层节点个数     int len=queue.size();     ArrayList<Integer> row=new ArrayList<>();     for(int i=0;i<len;i++){  //循环次数,也就是当前这一层节点个数   TreeNode temp=queue.poll();   if(temp.left!=null)queue.add(temp.left);   if(temp.right!=null)queue.add(temp.right);  row.add(temp.val);     }    res.add(row); } return res;    }}

63. 序列化二叉树

题目描述:请实现两个函数,分别用来序列化和反序列化二叉树。

解析:序列化是指将结构化的对象转化为字节流以便在网络上传输或写到磁盘进行永久存储的过程。反序列化是指将字节流转回结构化的对象的过程,是序列化的逆过程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7JVGAAD-1648472524145)(D:\Typora_files\序列化二叉树.png)]

序列化:只根据前序遍历的顺序来进行序列化,前序遍历是从根结点开始,在遍历二叉树碰到空指针时,就将其序列化为一个特殊字符,比如$,另外,结点的数值之间用一个特殊字符(比如,)进行分隔。比如对于以下的树,序列化为字符串:"1,2,4,$,$,$,3,5,$,$,6,$,$"

反序列化:实际就是一个递归解决每个子树的问题(也是前序遍历的逻辑生成树)。以"1,2,4,$,$,$,3,5,$,$,6,$,$"为例来分析反序列化的过程,第一个读出1,这是根结点的值,然后读出2,这是根结点的左孩子,同样接下来的4是值为2的结点的左孩子结点;接下来读出两个“”,说明值为4的结点左右孩子都是null,这是一个叶结点。然后回到值为2的结点,重建它的右子树,由于下一个字符是“”,说明值为4的结点左右孩子都是null,这是一个叶结点。然后回到值为2的结点,重建它的右子树,由于下一个字符是“”,说明值为2的结点的右孩子结点为null,这个结点的左右子树都重建完毕,接下来再次回溯就到了根结点,所以,左子树重构完毕。

from common import BiTNode# 若不写成类的形式,函数内部使用全局变量时,要先声明,global 变量名,然后再进行使用# 全局变量的修改,如果全局变量是int或者str,那么如果想要在函数中对函数变量进行修改,则需要先在函数内,声明其为global,再进行修改,如果是list或者dict则可以直接修改class Solution:    def __init__(self): self.position = -1    def serialize_tree(self, root): if root is None:     return '#,' res = "" res += str(root.data) + ',' res += self.serialize_tree(root.lchild) res += self.serialize_tree(root.rchild) return res    def de_serialize_tree(self, tree_s): if tree is None or len(tree_s) == 0:     return None tree_arr = tree_s.split(',') return self.de_serialize_tree_(tree_arr)    def de_serialize_tree_(self, tree_arr): self.position += 1 # if self.position >= len(tree_arr) or tree_arr[self.position] == '#': if tree_arr[self.position] == '#':     return None cur = BiTNode() cur.data = int(tree_arr[self.position]) cur.lchild = self.de_serialize_tree_(tree_arr) cur.rchild = self.de_serialize_tree_(tree_arr) return curif __name__ == '__main__':    tree = BiTNode()    node1 = BiTNode()    node2 = BiTNode()    node3 = BiTNode()    node4 = BiTNode()    node5 = BiTNode()    tree.data = 1    tree.lchild = node1    tree.rchild = node2    node1.data = 2    node2.data = 3    node1.lchild = node3    node3.data = 4    node2.lchild = node4    node2.rchild = node5    node4.data = 5    node5.data = 6    solution = Solution()    result = solution.serialize_tree(tree)    if result: result = result[0:-1]    copy_tree = solution.de_serialize_tree(result)    print(copy_tree.lchild.lchild.data)    print(copy_tree.rchild.lchild.data)

64. 二叉搜索树的第K个结点

题目描述:给定一棵二叉搜索树,请找出其中的第k小的结点。例如(5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

解析:首先可以知道二叉搜索树的特点:左结点的值<根结点的值<右结点的值。因此,到如下结论:如果按照中序遍历的顺序对一棵二叉搜索树进行遍历,那么得到的遍历序列就是递增排序的。因此,只要用中序遍历的顺序遍历一棵二叉搜索树,就很容易找出它的第k小的结点。

from common import BiTNodenum = 0def find_kth_node(root, k):    if root is None or k < 1: return None    # 先声明要使用的是全局变量    global num    num = k    return find_kth_node_(root)def find_kth_node_(root):    res = None    if root.lchild is not None: res = find_kth_node_(root.lchild)    if res is None: global num if num == 1:     res = root.data num -= 1    if res is None and root.rchild is not None: res = find_kth_node_(root.rchild)    return resif __name__ == '__main__':    test_tree = BiTNode()    test_tree.data = 5    node1 = BiTNode()    node2 = BiTNode()    node3 = BiTNode()    node4 = BiTNode()    node5 = BiTNode()    node6 = BiTNode()    node1.data = 3    node2.data = 7    node3.data = 2    node4.data = 4    node5.data = 6    node6.data = 8    test_tree.lchild = node1    test_tree.rchild = node2    node1.lchild = node3    node1.rchild = node4    node2.lchild = node5    node2.rchild = node6    print(find_kth_node(test_tree, 1))