[FreeCodeCamp笔记] Python 数据结构和算法1 二分搜索 Binary Search

Posted 有数可据

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[FreeCodeCamp笔记] Python 数据结构和算法1 二分搜索 Binary Search相关的知识,希望对你有一定的参考价值。

我以前学过数据结构和算法(data structure and algorithms. 现在普遍简称DSA),当时用的Robert Sedgewick的coursera课程。这位大神写的《算法(第四版)》,是算法的经典教材,可惜这本书900页,我直接被吓跑了。而coursera课程用的是java,我又不会java,所以课后习题做的异常艰苦。

这几天,我又想学一下数据结构和算法,但是我决定用python学。其实我也会c++和c#,但是python显然是最简单易用的语言,使用python可以节省很多时间。于是,我在网上找免费的资源,最后,我找到了一个12个小时的视频。这个视频是FreeCodeCamp.org提供的。FreeCodeCamp是一个免费提供程序员教学视频的网站,很多人通过它实现了转行。它的视频的特点是时间都很长,最短的可能也有2小时。比起其他的编程视频,它是一个比较系统的教材。这边的python数据结构和算法,一共有6课,分别是Binary Search, Binary Search Trees, Hash和字典,递归和动态规划,图,面试技巧。

我看了第一课,虽然Binary Search比较简单,也许很多人觉得不用再看了。不过这一课里面,有比较规范的解题过程。从单元测试,到通用算法,这些都能和python的语言特性相结合,而且这些都是容易被我们忽略的。

问题

爱丽丝有几张扑克牌。她将扑克牌按降序排列,然后将它们按顺序面朝下放在桌子上。她挑战鲍勃,让他翻出尽可能少的扑克牌,从中选出包含给定数字的扑克牌。编写一个函数来帮助 Bob 定位卡片。

你为什么要学习数据结构和算法

无论您是从事软件开发还是数据科学职业,几乎可以肯定的是,您会被要求解决编程问题,例如在技术面试或编码评估中反转链表或平衡二叉树。

然而,众所周知,作为软件开发人员,您在工作中几乎永远不会遇到这些问题。所以有理由想知道为什么在面试和编码评估中会问这样的问题。解决编程问题表现出以下特点:

  1. 你可以系统地思考一个问题,然后一步一步地系统地解决它。
  2. 您可以为您编写的程序设想不同的输入、输出和边缘情况。
  3. 您可以向同事清楚地传达您的想法并采纳他们的建议。
  4. 最重要的是,您可以将您的想法和想法转化为可读的工作代码。

在面试中测试的不是您对特定数据结构或算法的了解,而是您解决问题的方法。您可能无法解决问题并仍然通过面试,反之亦然。在本课程中,您将学习成功解决问题和清晰面试的技能。

方法

阅读问题后,您可能会对如何解决它有一些想法,您的第一直觉可能是开始编写代码。这不是最佳策略,由于编码错误,您最终可能会花费更长的时间来尝试解决问题,或者根本无法解决问题。

这是我们将用于解决问题的系统策略:

  1. 把问题说清楚。识别输入和输出格式。
  2. 提出一些示例输入和输出。尝试覆盖所有边缘情况。
  3. 提出问题的正确解决方案。用简单的英语说出来。
  4. 实施解决方案并使用示例输入对其进行测试。修复错误,如果有的话。
  5. 分析算法的复杂性并找出效率低下的地方(如果有)。
  6. 应用正确的技术来克服低效率。重复步骤 3 到 6。

*“应用正确的技术”*是常用数据结构和算法知识派上用场的地方。

解决方案

1. 把问题说清楚。识别输入和输出格式。

在编码挑战和面试中,您经常会遇到详细的单词问题。第一步是用抽象的术语清楚而准确地陈述问题。

例如,在这种情况下,我们可以将扑克牌序列表示为数字列表。翻出一张特定的卡片,就相当于访问了列表对应位置的数字的值。

现在可以将问题表述如下:

问题

我们需要编写一个程序来查找给定数字在按降序排列的数字列表中的位置。我们还需要尽量减少访问列表中元素的次数。

输入

  1. cards:按降序排列的数字列表。例如[13, 11, 10, 7, 4, 3, 1, 0]
  2. query: 一个数字,要确定其在数组中的位置。例如7

输出

  1. position:query在列表中的位置cards。例如3在上述情况下(从 开始计数0

基于以上内容,我们现在可以创建函数的签名:

def locate_card(cards, query):
    pass

2. 单元测试

我们的函数应该能够处理我们传递给它的任何有效输入集。以下是我们可能会遇到的一些可能变化的列表:

  1. 该数字query出现在列表中间的某个位置cards
  2. query是 中的第一个元素cards
  3. query是 中的最后一个元素cards
  4. 该列表cards仅包含一个元素,即query
  5. 该列表cards不包含 number query
  6. 该列表cards为空。
  7. 该列表cards包含重复数字。
  8. 该数字query出现在 中的多个位置cards
  9. (你能想到更多的变化吗?)
tests = []

# query occurs in the middle
# 在中间的情况
tests.append(
    'input': 
        'cards': [13, 11, 10, 7, 4, 3, 1, 0],
        'query': 1
    ,
    'output': 6
)

# query is the first element
# 第一张扑克牌就是我们要找的
tests.append(
    'input': 
        'cards': [4, 2, 1, -1],
        'query': 4
    ,
    'output': 0
)

# query is the last element
# 最后一张扑克牌
tests.append(
    'input': 
        'cards': [3, -1, -9, -127],
        'query': -127
    ,
    'output': 3
)

# cards contains just one element, query
# 只有一张扑克牌
tests.append(
    'input': 
        'cards': [6],
        'query': 6
    ,
    'output': 0 
)

# cards does not contain query 
# 找不到
tests.append(
    'input': 
        'cards': [9, 7, 5, 2, -9],
        'query': 4
    ,
    'output': -1
)

# cards is empty
# 扑克牌队列是空的
tests.append(
    'input': 
        'cards': [],
        'query': 7
    ,
    'output': -1
)

# numbers can repeat in cards
# 有重复的数字
tests.append(
    'input': 
        'cards': [8, 8, 6, 6, 6, 6, 6, 3, 2, 2, 2, 0, 0, 0],
        'query': 3
    ,
    'output': 7
)

# query occurs multiple times
# 被查找的扑克牌出现很多次
tests.append(
    'input': 
        'cards': [8, 8, 6, 6, 6, 6, 6, 6, 3, 2, 2, 2, 0, 0, 0],
        'query': 6
    ,
    'output': 2
)

让我们看一下我们的单元测试。

>>>tests
['input': 'cards': [13, 11, 10, 7, 4, 3, 1, 0], 'query': 7, 'output': 3,
 'input': 'cards': [13, 11, 10, 7, 4, 3, 1, 0], 'query': 1, 'output': 6,
 'input': 'cards': [4, 2, 1, -1], 'query': 4, 'output': 0,
 'input': 'cards': [3, -1, -9, -127], 'query': -127, 'output': 3,
 'input': 'cards': [6], 'query': 6, 'output': 0,
 'input': 'cards': [9, 7, 5, 2, -9], 'query': 4, 'output': -1,
 'input': 'cards': [], 'query': 7, 'output': -1,
 'input': 'cards': [8, 8, 6, 6, 6, 6, 6, 3, 2, 2, 2, 0, 0, 0], 'query': 3,
  'output': 7,
 'input': 'cards': [8, 8, 6, 6, 6, 6, 6, 6, 3, 2, 2, 2, 0, 0, 0],
   'query': 6,
  'output': 2]

3. 提出问题的正确解决方案。用简单的语言表达出来。

我们的首要目标应该始终是为问题提出正确的解决方案,这可能是最有效的解决方案。一个问题的最简单或最明显的解决方案,通常涉及检查所有可能的答案,称为蛮力解决方案。

在这个问题中,想出一个正确的解决方案是很容易的:Bob 可以简单地将卡片一张一张地翻过来,直到他找到一张上面有给定数字的卡片。下面是我们如何实现它:

  1. 创建一个position值为 0的变量。
  2. 检查是否在索引数量positioncard平等query
  3. 如果是,position就是答案并且可以从函数返回
  4. 如果不是,则将 的值增加position1,并重复步骤 2 到 5,直到我们到达最后一个位置。
  5. 如果未找到该号码,则返回-1

线性搜索算法:恭喜,我们刚刚编写了我们的第一个算法!算法只是一个语句列表,可以将其转换为代码并由计算机在不同的输入集上执行。这种特定的算法称为线性搜索,因为它涉及以线性方式搜索列表,即元素一个元素。

**提示:**在开始编码之前,始终尝试用自己的语言表达(说或写)算法。它可以根据您的需要简短或详细。写作是清晰思考的好工具。您可能会发现解决方案的某些部分难以表达,这表明您可能无法清楚地思考它。你越能清楚地表达你的想法,你就越容易转化为代码。

4. 实施解决方案并使用示例输入对其进行测试,修复错误。

呼!我们终于准备好实施我们的解决方案了。到目前为止我们所做的所有工作肯定会派上用场,因为我们现在正是我们想要我们的函数做的事情,并且我们有一种简单的方法可以在各种输入上测试它。

这是实现该功能的第一次尝试。

def locate_card(cards, query):
    # Create a variable position with the value 0
    position = 0
    
    # Set up a loop for repetition
    while True:
        
        # Check if element at the current position matche the query
        if cards[position] == query:
            
            # Answer found! Return and exit..
            return position
        
        # Increment the position
        position += 1
        
        # Check if we have reached the end of the array
        if position == len(cards):
            
            # Number not found, return -1
            return -1

让我们用第一个测试用例来测试这个函数

>>>result = locate_card(test['input']['cards'], test['input']['query'])
>>>result
3

好极了!结果与输出匹配。

接着,我们测试所有的test case。

def test_all(tests, func):
    for test in tests:
        result = func(**test['input'])
        if result == test['output']:
            print(f"pass, expected: test['output'], actual: result")
        else:
            print(f"fail, expected: test['output'], actual: result")

我们现在运行所有的单元测试。

>>>test_all(tests, locate_card)
pass, expected: 3, actual: 3
pass, expected: 6, actual: 6
pass, expected: 0, actual: 0
pass, expected: 3, actual: 3
pass, expected: 0, actual: 0
pass, expected: -1, actual: -1

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-54-e822f482ec2b> in <module>
----> 1 test_all(tests, locate_card)

<ipython-input-53-680d62f7d94a> in test_all(tests, func)
      1 def test_all(tests, func):
      2     for test in tests:
----> 3         result = func(**test['input'])
      4         if result == test['output']:
      5             print(f"pass, expected: test['output'], actual: result")

<ipython-input-41-9ed30c367c36> in locate_card(cards, query)
      7 
      8         # Check if element at the current position matche the query
----> 9         if cards[position] == query:
     10 
     11             # Answer found! Return and exit..

IndexError: list index out of range


我们发现,第7个单元测试报错了。因为这里cards为空,所以试图取得第一个元素失败。

我们可以增加一个检查position是否合法的语句。

def locate_card(cards, query):
    position = 0
    while position < len(cards):
        if cards[position] == query:
            return position
        position += 1
    return -1

然后,我们再测试。

>>>test_all(tests, locate_card)
pass, expected: 3, actual: 3
pass, expected: 6, actual: 6
pass, expected: 0, actual: 0
pass, expected: 3, actual: 3
pass, expected: 0, actual: 0
pass, expected: -1, actual: -1
pass, expected: -1, actual: -1
pass, expected: 7, actual: 7
pass, expected: 2, actual: 2

不错,所有的单元测试都通过了。

5. 分析算法的复杂性并找出效率低下的地方(如果有)。

回想一下原始问题中的陈述:“爱丽丝挑战鲍勃,让他翻出尽可能少的卡片,从中选出包含给定数字的卡片。” 我们将这个要求重申为:“最小化我们访问列表中元素的次数cards

在我们最小化数量之前,我们需要一种方法来衡量它。由于我们在每次迭代中访问一个列表元素一次,对于一个大小的列表,N我们最多访问列表中的元素N。因此,N在最坏的情况下,Bob 可能需要翻转卡片,才能找到所需的卡片。

假设他每分钟只允许翻一张牌,如果桌子上放了 30 张牌,他可能需要 30 分钟才能找到所需的牌。这是他能做到的最好的吗?鲍勃可以通过只翻出 5 张牌而不是 30 张牌来得出答案吗?

与找到完成计算机程序执行所需的时间、空间或其他资源量有关的研究领域称为算法分析。找出解决给定问题的最佳算法的过程称为算法设计和优化

复杂性和大 O 符号

算法的复杂性是算法对于给定大小的输入所需的时间和/或空间量的度量,例如N。除非另有说明,否则术语复杂性总是指最坏情况的复杂性(即程序/算法处理输入所花费的最高可能时间/空间)。

在线性搜索的情况下:

  1. 该算法的时间复杂度cN针对某个固定常数的c,它取决于我们在每次迭代中执行的操作数量以及执行一条语句所花费的时间。时间复杂度有时也称为算法的运行时间
  2. 空间复杂度是某个常数c'(独立的N),因为我们只需要一个变量position来迭代通过数组,它占用计算机的内存(RAM)的恒定空间。

大 O 表示法:最坏情况的复杂性通常使用大 O 表示法表示。在 Big O 中,我们去掉了固定常数和变量的较低幂以捕捉输入大小与算法复杂度之间关系的趋势,即如果算法的复杂度为cN^3 + dN^2 + eN + f,则在 Big O 符号中表示为O(N^3)

因此,线性搜索的时间复杂度为O(N),其空间复杂度为O(1)

6. 应用正确的技术来克服低效率。重复步骤 3 到 6。

目前,我们只是一张一张地翻阅卡片,甚至没有利用它们被排序的面孔。这称为蛮力方法。

如果 Bob 能在第一次尝试时以某种方式猜出这张牌那就太好了,但是当所有的牌都翻过来时,根本不可能猜出正确的牌。

下一个最好的想法是随机选择一张卡片,并使用列表已排序的事实来确定目标卡片位于它的左侧还是右侧。事实上,如果我们选择中间卡,我们可以将要测试的附加卡数量减少到列表大小的一半。然后,我们可以简单地对每一半重复这个过程。这种技术称为二分查找。

7. 提出问题的正确解决方案。用简单的英语说出来。

以下是二分搜索如何应用于我们的问题:

  1. 找到列表的中间元素。
  2. 如果与查询到的号码匹配,则返回中间位置作为答案。
  3. 如果小于查询的数,则搜索列表的前半部分
  4. 如果大于查询的数,则搜索列表的后半部分
  5. 如果没有更多元素剩余,则返回 -1。

8. 实施解决方案并使用示例输入对其进行测试。修复错误

def locate_card(cards, query):
    lo, hi = 0, len(cards) - 1
    
    while lo <= hi:
        mid = (lo + hi) // 2
        mid_number = cards[mid]
        
        print("lo:", lo, ", hi:", hi, ", mid:", mid, ", mid_number:", mid_number)
        
        if mid_number == query:
            return mid
        elif mid_number < query:
            hi = mid - 1  
        elif mid_number > query:
            lo = mid + 1
    
    return -1

单元测试

>>>test_all(tests, locate_card)
lo: 0 , hi: 7 , mid: 3 , mid_number: 7
pass, expected: 3, actual: 3
lo: 0 , hi: 7 , mid: 3 , mid_number: 7
lo: 4 , hi: 7 , mid: 5 , mid_number: 3
lo: 6 , hi: 7 , mid: 6 , mid_number: 1
pass, expected: 6, actual: 6
lo: 0 , hi: 3 , mid: 1 , mid_number: 2
lo: 0 , hi: 0 , mid: 0 , mid_number: 4
pass, expected: 0, actual: 0
lo: 0 , hi: 3 , mid: 1 , mid_number: -1
lo: 2 , hi: 3 , mid: 2 , mid_number[FreeCodeCamp笔记] Python 数据结构和算法1 二分搜索 Binary Search

[FreeCodeCamp笔记] Python 数据结构和算法1 二分搜索 Binary Search

RX编程笔记:在FreeCodeCamp的学习

RX学习笔记:FreeCodeCamp的JavaScript基本算法挑战

freecodecamp笔记--js

freecodecamp 基础算法题笔记