python技术面试题(十五)--算法

简介: python技术面试题(十五)--算法


每日分享

If you lose, don't lose the lesson.

直译:如果你输了,不要失去教训。

意译:吃一堑长一智。

小闫语录

输了并不代表一无所有,你所经历的同样宝贵。如果你没有总结教训,只是沉浸在阴霾中,这样你就真的输了。

昨天公众号突然来了好多新的小伙伴,希望大家学有所成,也希望我的文章可以帮助到你。同时谢谢大家的关注。新来的小伙伴一定要先看使用指南。在文末的『优质文章推荐』第一篇。历史文章通过关键字查询方法见菜单栏『来看看嘛』中。方便你更好的使用本公众号。



算法

1. 算法的五大特性

输入:算法具有0个或多个输入。

输出:算法至少有一个输出。

有穷性:算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成。

确定性:算法中的每一步都有确定的含义,不会出现二义性。

可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限次数完成。

2. 冒泡排序的思想

冒泡排序的过程,就好像咱们喝汽水时,那些小气泡一点一点从下往上的冒,最后到了最顶部。这只是一种形象的类比,用实际的例子来说明一下。假如有一个列表,其中的数字是无序排列的,通过冒泡要实现的结果就是将列表中的数字从小到大排序。

那么怎么实现呢?我们可以将列表中左侧第一个和第二个数字先进行比较,将较小的排在左侧;然后再比较第二个和第三个数字,较小的排在左侧,再比较第三个和第四个......将列表中的数字第一轮比较完之后,最大的数,排在了列表的最尾端。然后重复上面的步骤,但是尾端最大的数不再参与比较,一轮一轮的比较完之后,实现将列表中的数字从小到大排序后的效果。这样是不是最小的数一点一点从后往前冒了呢?

冒泡的最优时间复杂度为O(n),最坏时间复杂度为O(n^2)。空间复杂度为O(1)。

那么下面使用代码实现一个冒泡排序:

def bubble_sort(alist):
    for j in range(0,len(alist)-1):
        for i in range(j):
            if alist[i] > alist[i+1]:
                alist[i],alist[i+1] = alist[i+1],alist[i]
                print(alist)
alist = [66,0,1,55,3,99,100,2553245]
bubble_sort(alist)
print(alist)
---------------结果--------------
[0, 66, 1, 55, 3, 99, 100, 2553245]
[0, 1, 66, 55, 3, 99, 100, 2553245]
[0, 1, 55, 66, 3, 99, 100, 2553245]
[0, 1, 55, 3, 66, 99, 100, 2553245]
[0, 1, 3, 55, 66, 99, 100, 2553245]
[0, 1, 3, 55, 66, 99, 100, 2553245]

细心的人已经看出来了,输出的最后两个列表是一样的。也就是在排序的过程中有一些步骤是无用的,因为比较的过程中有些元素已经实现了有序,不需要再比较。这是一个需要优化的地方,感兴趣的小伙伴可以自己手动优化一下。优化过后你会发现还有需要优化的地方,如果感兴趣,可以查找一下关于鸡尾酒排序的资料。

3. 快速排序的思想

快速排序的方法和冒泡排序类似,也属于交换排序。也就是通过不断的比较元素之间的大小,同时不断的交换元素的位置,实现排序的效果。那么它为什么叫快速排序呢?因为它快啊!(......很欠揍的样子)

我们简单的了解一下快速排序。快速排序就是先找到一个基准(一般来说拿列表中的第一个元素先做为基准),然后利用这个基准,将列表中的元素分为两部分。一部分(这部分中的元素每一个都要比基准大)放在基准的右侧;另一部分(这一部分中的元素都比基准小)放在基准左侧。第一轮划分完毕,第二轮会将左部分和右部分再次进行第一轮的步骤。然后不断的划分啊划分啊划分啊......直到最后列表中的元素变成了有序,就结束排序。

看到了上面的步骤是不是发现一个问题?这不就是我们的递归思想吗?对的,稍后我们就利用递归的方法实现一个快速排序。

快速排序的最优时间复杂度为O(nlogn),最坏时间复杂度为O(n^2),空间复杂度为O(logn)。它是不稳定的。

下面使用代码实现一个快速排序:

# ==============快速排序==================
def quick_sort(alist, start, end):
    # 递归的退出条件
    if start >= end:
        return
    # 设定起始元素为要寻找位置的基准元素
    mid = alist[start]
    # low为序列左边的,由左向右移动的游标
    low = start
    # high为序列右边的,由右向左移动的游标
    high = end
    while low < high:
        # 如果low与high未重合,high指向的元素比基准元素大,则high向左移动
        while low < high and alist[high] >= mid:
            high -= 1
        # 当high指向的元素比基准元素小了
        # 将high指向的元素放到low的位置上
        alist[low] = alist[high]
        # 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
        while low < high and alist[low] < mid:
            low += 1
        # 当low指向的元素比基准元素大了
        # 将low指向的元素放到high的位置上
        alist[high] = alist[low]
    # 退出循环后,low与high重合,此时所指位置为基准元素的正确位置
    # 将基准元素放到该位置
    alist[low] = mid
    # 对基准元素左边的子序列进行快速排序
    quick_sort(alist, start, low-1)
    # 对基准元素右边的子序列进行快速排序
    quick_sort(alist, low+1, end)
alist = [54,26,93,17,77,31,44,55,20]
quick_sort(alist,0,len(alist)-1)
print(alist)

python中没有指针的概念,上述代码中的游标就类似于指针的效果。

本次使用的方法和c语言中的“挖坑法”类似。当然还有其他的很多方法,大家可以查阅相关资料进行学习。

4. 插入排序

插入排序是一种简单直观的排序方法,我想说一句废话(插入排序就是通过插入来实现排序),想了想还是忍住了。插入排序的思路是什么样的呢?下面且听我慢慢道来。

有一个无序列表,让我们将其中的元素从小到大进行排序。使用插入排序,首先将从左到右的第一个元素所在的区域叫做有序区,其他的元素在的区域叫做无序区。然后将无序区中的元素从左到右开始取,取出来一个元素就将其放在有序区的合适位置(比如无序区取了一个3,有序区有两个数字1和4,那么我们就将3放到1和4之间)。不断的从无序区取,向有序区合适位置插,直到最后无序区没有值了,列表也就变成了有序列表。

最优时间复杂度为O(n),最坏时间复杂度为O(n^2),具有稳定性。

那么用代码实现一个插入排序:

def insert_sort(alist):
    # 从第二个位置,即下标为1的元素开始向前插入
    for i in range(1, len(alist)):
        # 从第i个元素开始向前比较,如果小于前一个元素,交换位置
        for j in range(i, 0, -1):
            if alist[j] < alist[j-1]:
                alist[j], alist[j-1] = alist[j-1], alist[j]

5. 选择排序

有了上面算法的基础,选择排序理解就没那么难了。

同样有一个无序列表,我们需要对其从小到大进行排序。使用选择排序的话,我们先从列表中挑选出一个最大值,然后将其和列表最尾端的值进行调换。最后一个元素就是最大值,毫无悬念,它找到了自己的最终归属,不需要再参与排序(它所在的区域叫做有序区,剩余元素处于无序区)。剩下的元素再挑选一个最大值,将其放到放到有序区的合适位置......不断的重复以上步骤,直到所有的元素放到有序区,得到了一个有序列表,完成我们的需求。

最优时间复杂度为O(n^2),最坏时间复杂度为O(n^2),不稳定。

代码实现:

def selection_sort(alist):
    n = len(alist)
    # 需要进行n-1次选择操作
    for i in range(n-1):
        # 记录最小位置
        min_index = i
        # 从i+1位置到末尾选择出最小数据
        for j in range(i+1, n):
            if alist[j] < alist[min_index]:
                min_index = j
        # 如果选择出的数据不在正确位置,进行交换
        if min_index != i:
            alist[i], alist[min_index] = alist[min_index], alist[i]

6. 希尔排序

希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 DL.Shell于1959年提出而得名。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

最优时间复杂度根据步长序列的不同而不同,最坏时间复杂度为O(n^2),不稳定。

看完后你心中肯定有一万头草泥马在奔腾了,一定想说『说人话!』好吧,还是用个例子说明一下希尔排序的思想吧.....

希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。将数组转换成表是为了更好地理解这算法,算法本身还是使用数组进行排序。

例如,假设有这样一组数[ 13,14,94,33,82,25,59,94,65,23,45,27,73,25,39,10 ],如果我们以步长为5开始进行排序,我们可以通过将这些数放在有5列的表中来更好地描述算法,这样他们就像下面了:

13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10

然后我们对每列进行排序:

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

将上述四行数字,依序接在一起时我们得到:[ 10,14,73,25,23,13,27,94,33,39,25,59,94,65,82,45 ]。这时10已经移至正确位置了,然后再以3为步长进行排序:

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

再次对列进行排序,变为下面这样:

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94

然后将上面的6行数字在衔接在一起:[ 10,14,13,25,23,33,27,25,59,39,65,73,45,94,82,94 ]。然后我们以1为步长进行排序,此时就是简单的插入排序了。最后结果为:

[10, 13, 14, 23, 25, 25, 27, 33, 39, 45, 59, 65, 73, 82, 94, 94]

我们使用代码实现一个希尔排序:

def shell_sort(alist):
    n = len(alist)
    # 初始步长
    gap = n // 2
    while gap > 0:
        # 按步长进行插入排序
        for i in range(gap, n):
            j = i
            # 插入排序
            while j>=gap and alist[j-gap] > alist[j]:
                alist[j-gap], alist[j] = alist[j], alist[j-gap]
                j -= gap
        # 得到新的步长
        gap = gap // 2
alist = [ 13,14,94,33,82,25,59,94,65,23,45,27,73,25,39,10 ]
shell_sort(alist)
print(alist)

7. 桶排序

当列表中的数值取值范围太大,或者不是整数的时候,可以使用桶排序来解决问题。

桶排序基本思想就是将一个大区间划分成n个相同大小的子区间,称为桶。将n个记录分布到各个桶中。如果有多于一个记录被分到同一个桶中,需要进行桶内排序。最后依次把各个桶中的记录列出来就得到有序序列。

其实就是预先准备很多“桶”,然后把无序列表中的数,按照对应的区间放入桶中,桶里的数超过两个了,那么桶内再进行排序,然后将所有的数按照“桶”的顺序排列出来(一个桶内有多个数据的,按照桶内排好的顺序依次取出)。这是典型的以空间换时间,进行排序的数变少了,效率高了,时间就短了,但是占据了很多空间。

8. 归并排序

归并排序采用了分而治之的思想,先递归分解无序列表,再合并列表。我们先来看一下两个有序的列表如何进行合并:

1.我们有两个列表:

list1 = [3,5,7,8]
list2 = [1,4,9,10]

2.为了合并,我们需要再建立一个空列表。

list = []

3.然后就是将两个有序列表list1和list2分别从左到右比较两个列表中哪一个值比较小,然后复制进新的列表中

# 一开始新列表是空的
['3',5,7,8] ['1',4,9,10] []
# 然后两个指针分别指向第一个元素,进行比较,显然,1比3小,所以把1复制进新列表中:
['3',5,7,8] [1,'4',9,10] [1]
# list2的指针后移,再进行比较,这次是3比较小:
[3,'5',7,8] [1,'4',9,10] [1,3]
# 同理,我们一直比较到两个列表中有某一个先到末尾为止,在我们的例子中,list1先用完。
[3,5,7,8] [1,4,'9',10] [1,3,4,5,7,8]
# 最后把list2中剩余的元素复制进新列表即可。
[1,3,4,5,7,8,9,10]

为了突出比较的过程,我们将比较时的数字加了引号,其实它是int类型。由于前提是这两个列表都是有序的,所以整个过程是很快的。

上面就是归并排序中最主要的想法和实现了。接下来我们完整的对归并排序进行描述:

有一个列表,我们将其对半分,不断的重复这个过程,问题的规模就越来越小,直到分成了一个一个的数字(一个数字就相当于排好序了),接下来呢,就是类似于上面的过程了,两两合并,然后再两两合并,直到最后合并为一个有序的列表。正所谓天下之势,分久必合合久必分。现在有了一个了解了吧?

最优时间复杂度为O(nlogn),最坏时间复杂度为O(nlogn),稳定。

代码实现:

def merge_sort(alist):
    if len(alist) <= 1:
        return alist
    # 二分分解
    num = len(alist)//2
    left = merge_sort(alist[:num])
    right = merge_sort(alist[num:])
    # 合并
    return merge(left,right)
def merge(left, right):
    '''合并操作,将两个有序数组left[]和right[]合并成一个大的有序数组'''
    #left与right的下标指针
    l, r = 0, 0
    result = []
    while l<len(left) and r<len(right):
        if left[l] < right[r]:
            result.append(left[l])
            l += 1
        else:
            result.append(right[r])
            r += 1
    result += left[l:]
    result += right[r:]
    return result
alist = [54,26,93,17,77,31,44,55,20]
sorted_alist = merge_sort(alist)
print(sorted_alist)
---------------------------
[17, 20, 26, 31, 44, 54, 55, 77, 93]

9. 计数排序

对于那些数值取值范围不是很大的整数构成的列表怎么进行排序呢?那就是计数排序了,它的性能甚至会超过那些O(nlogn)的排序。神奇吧?

它是通过列表的下标来确定元素的正确位置。假如有一个列表,它由20个随机整数构成,且每个元素的取值范围从0到10。利用计数排序,我们可以建立一个长度为11的新列表,新列表下标从0到10上的元素初始值都为0。然后遍历无序的列表,每一个整数按照其值在新列表中进行对号入座,新列表中对应下标的元素进行加1操作(比如整数2,那么新列表中下标为2的元素由原来的0加1后变为了1;如果再有个整数2,那么下标为2的元素由原来的1加1变为了2。类似这样的操作,直到无序列表中所有的整数遍历完)。

新列表中每一个下标位置的值,代表的是无序列表中该整数出现的次数(下标的值与无序列表中对应的整数相等)。最后将新列表进行遍历,注意输出的是下标值,下标对应的元素是几,该下标就输出几次,遍历完成,无序列表变为了有序。

不适用于计数排序的情况

1.当列表中最大值和最小值差距过大时;

2.当列表中元素不是整数时;

如果原始列表的规模是N,最大值和最小值的差是M的话,计数排序的时间复杂度和空间复杂度是多少呢?

答:时间复杂度为O(N+M),空间复杂度如果只考虑统计列表大小的话,为O(M)。

相关文章
|
6天前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
19 2
|
6天前
|
机器学习/深度学习 人工智能 算法
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
垃圾识别分类系统。本系统采用Python作为主要编程语言,通过收集了5种常见的垃圾数据集('塑料', '玻璃', '纸张', '纸板', '金属'),然后基于TensorFlow搭建卷积神经网络算法模型,通过对图像数据集进行多轮迭代训练,最后得到一个识别精度较高的模型文件。然后使用Django搭建Web网页端可视化操作界面,实现用户在网页端上传一张垃圾图片识别其名称。
29 0
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
|
6天前
|
机器学习/深度学习 人工智能 算法
【手写数字识别】Python+深度学习+机器学习+人工智能+TensorFlow+算法模型
手写数字识别系统,使用Python作为主要开发语言,基于深度学习TensorFlow框架,搭建卷积神经网络算法。并通过对数据集进行训练,最后得到一个识别精度较高的模型。并基于Flask框架,开发网页端操作平台,实现用户上传一张图片识别其名称。
22 0
【手写数字识别】Python+深度学习+机器学习+人工智能+TensorFlow+算法模型
|
6天前
|
机器学习/深度学习 人工智能 算法
基于深度学习的【蔬菜识别】系统实现~Python+人工智能+TensorFlow+算法模型
蔬菜识别系统,本系统使用Python作为主要编程语言,通过收集了8种常见的蔬菜图像数据集('土豆', '大白菜', '大葱', '莲藕', '菠菜', '西红柿', '韭菜', '黄瓜'),然后基于TensorFlow搭建卷积神经网络算法模型,通过多轮迭代训练最后得到一个识别精度较高的模型文件。在使用Django开发web网页端操作界面,实现用户上传一张蔬菜图片识别其名称。
26 0
基于深度学习的【蔬菜识别】系统实现~Python+人工智能+TensorFlow+算法模型
|
10天前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
28 2
|
20天前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
20 3
|
22天前
|
机器学习/深度学习 人工智能 算法
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
车辆车型识别,使用Python作为主要编程语言,通过收集多种车辆车型图像数据集,然后基于TensorFlow搭建卷积网络算法模型,并对数据集进行训练,最后得到一个识别精度较高的模型文件。再基于Django搭建web网页端操作界面,实现用户上传一张车辆图片识别其类型。
66 0
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
|
27天前
|
机器学习/深度学习 算法 Java
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)