《算法图解》学习笔记—第1-4章

第1章 算法简介

算法是一组完成任务的指令,任何代码片段都可视为算法。

1.1 二分查找

二分查找是一种算法,其输入是一个有序的元素列表。如果要查找的元素包含在列表中,二分查找返回其位置;否则返回null。

应用实例:在电话簿中找一个名字以K打头的人;登录Facebook时,Facebook必须核实你是否有其网站的账户,须在其数据库中查找你的用户名;以最少的次数猜到一个1~100之间的数字。
在这里插入图片描述
对于包含n个元素的列表,用二分查找最多需要 l o g 2 n log_2^n log2n步,而简单查找最多需要 n n n步。二分查找的Python代码如下:

def binary_search(list,item):
    low=0
    high=len(list)-1

    while low <= high:
          mid = (low + high) /2 #如果(low + high)不是偶数,Python自动将mid向下取整
          guess = list[mid]
          if guess == item: #找到了元素
             return mid
          if guess > item: #猜的数字大了
             high = mid - 1
          else:            #猜的数字小了
             low = mid + 1
     return None           #没有指定元素

1.2 大O表示法

大O表示法是一种特殊的表示法,指出了算法的速度有多快。
仅知道算法需要多长时间才能运行完毕还不够,还需知道运行时间如何随列表增长而增加。这正是大O表示法的用武之地。
大O表示法指出了算法有多快。例如,假设列表包含n个元素。简单查找需要检查每个元素,因此需要执行n次操作。使用大O表示法,这个运行时间为O( n n n)。单位秒呢?没有——大O表示法指的并非以秒为单位的速度。大O表示法让你能够比较操作数,它指出了算法运行时间的增速。
大O 表示法指出了最糟情况下的运行时间。
在这里插入图片描述

一些常见的大O运行时间

  • O( log ⁡ n \log n logn),也叫对数时间,这样的算法包括二分查找。

  • O( n n n),也叫线性时间,这样的算法包括简单查找。

  • O( n ∗ l o g n n * log n nlogn),这样的算法包括快速排序——一种速度较快的排序算法。

  • O( n 2 n^2 n2),这样的算法包括选择排序——一种速度较慢的排序算法。

  • O( n ! n! n!),这样的算法包括旅行商问题的解决方案——一种非常慢的算法。
    在这里插入图片描述

  • 算法的速度指的并非时间,而是操作数的增速。

  • 谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。

  • 算法的运行时间用大O表示法表示。

  • O( l o g n log n logn)比O( n n n)快,当需要搜索的元素越多时,前者比后者快得越多。

  • 大O表示法不考虑乘以、除以、加上或减去的数字。下面这些都不是正确的大O运行时间:O( n n n + 26)、O( n n n - 26)、O( n n n * 26)、O( n n n / 26),它们都应表示为O( n n n),通常不考虑常量。

第2章 选择排序

2.1内存的工作原理

在这里插入图片描述
fe0ffeeb是一个内存单元的地址。
需要将数据存储到内存时,你请求计算机提供存储空间,计算机给你一个存储地址。需要存储多项数据时,有两种基本方式——数组和链表。

2.2 数组和链表

有时候,需要在内存中存储一系列元素。假设你要编写一个管理待办事项的应用程序,为此需要将这些待办事项存储在内存中。
在这里插入图片描述
使用数组意味着所有待办事项在内存中都是相连的(紧靠在一起的)。再添加代办事项时,需要请求计算机重新分配一块可容纳4个待办事项的内存,再将所有待办事项都移到那里。

在这里插入图片描述
链表中的元素可存储在内存的任何地方。链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。在链表中添加元素很容易:只需将其放入内存,并将其地址存储到前一个元素中。

在这里插入图片描述
需要随机地读取元素时,数组的效率很高,因为可迅速找到数组的任何元素。在链表中,元素并非靠在一起的,你无法迅速计算出第五个元素的内存地址,而必须先访问第一个元素以获取第二个元素的地址,再访问第二个元素以获取第三个元素的地址,以此类推,直到访问第五个元素。

  • 元素的位置称为索引。因此,不说“元素20的位置为1”,而说“元素20位于索引1处”

中间插入

需要在中间插入元素时,使用链表,插入元素很简单,只需修改它前面的那个元素指向的地址。
在这里插入图片描述
而使用数组时,则必须将后面的元素都向后移。如果没有足够的空间,可能还得将整个数组复制到其他地方!因此,当需要在中间插入元素时,链表是更好的选择。
在这里插入图片描述

删除

如果你要删除元素呢?链表也是更好的选择,因为只需修改前一个元素指向的地址即可。而使用数组时,删除元素后,必须将后面的元素都向前移。

运行时间

下面列出了常见的数组和链表操作的运行时间。
在这里插入图片描述

2.3 选择排序

将数组元素按从小到大的顺序排列。先编写一个用于找出数组中最小元素的函数。

def findSmallest(arr):
    smallest = arr[0] #存储最小的值
    smallest_index = 0 #存储最小元素的索引
    for i in range(1,len(arr)):
        if arr[i] < smallest
           smallest = arr[i]
           smallest_index = i
    return smallest_index

现在可以使用这个函数来编写选择排序算法了。

def selectionSort(arr): #对数组进行排序
    newArr = []
    for i in range (len(arr)):
        smallest = findSmallest(arr)
        newArr.append(arr.pop(smallest)) #pop 移除列表中一个元素并返回该元素的值
    return newArr

选择排序的时间为O( n 2 n^2 n2)。第一次需要检查n个元素,但随后检查的元素数依次为 n − 1 n - 1 n1, n – 2 n – 2 n2, …, 1。平均每次检查的元素数为1/2 × n n n,因此运行时间为O( n × 1 / 2 × n n × 1/2 × n n×1/2×n)。但大O表示法省略诸如1/2这样的常数,因此简单地写作O( n × n n × n n×n)或O( n 2 n^2 n2)。
在这里插入图片描述

第3章 递归

算法是一组完成任务的指令,任何代码片段都可视为算法。

3.1 递归介绍

盒子中找钥匙
在这里插入图片描述
解决方案一:while 循环
在这里插入图片描述

def look_for_key(main_box):
    pile = main_box.make_a_pile_to_look_through()
    while pile is not empty:
        box = pile.grab_a_box()
        for item in box:
            if item.is_a_box():
               pile.append(item)
            elif item.is_a_key():
               print "found the key!"

解决方案二:递归——函数调用自己
在这里插入图片描述

def look_for_key(box):
    for item in box:
        if item.is_a_box():
           look_for_key(item)
        elif item.is_a_key():
           print "found the key!"

3.2 基线条件和递归条件

编写递归函数时,必须告诉它何时停止递归。正因为如此,每个递归函数都有两部分:基线条件(base case)和递归条件(recursive case)。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。

3.3 栈

栈是一种简单的数据结构。有两种操作:压入(插入)和弹出(删除并读取)。
在这里插入图片描述

调用栈

栈用于存储多个函数的变量时,被称为调用栈(call stack)。
每当你调用函数时,计算机会将函数调用涉及的所有变量的值存储到内存中。计算机使用一个栈来表示这些内存块,其中第二个内存块位于第一个内存块上面。当调用另一个函数时,当前函数暂停并处于未完成状态。
在这里插入图片描述

递归调用栈

下面是计算阶乘的递归函数。

def fact(x):
    if x == 1:
       return 1
    else:
       return x * fact(x-1)
  • 栈顶的方框指出了当前执行到了什么地方。
  • 每个fact调用都有自己的x变量。在一个函数调用中不能访问另一个 x x x的变量。

在这里插入图片描述
使用栈虽然很方便,但是也要付出代价:存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。解决方法:1.重新编写代码,转而使用循环。2.使用尾递归

第4章 快速排序

4.1 分而治之

分而治之(divide and conquer,D&C)——一种著名的递归式问题解决方法。D&C并非可用于解决问题的算法,而是一种解决问题的思路。D&C的工作原理:
(1) 找出简单的基线条件;
(2) 确定如何缩小问题的规模,使其符合基线条件。

示例:给定一个数字数组,需要将这些数字相加,并返回结果。
在这里插入图片描述

使用循环很容易完成这种任务。

def sum(arr):
    total = 0
    for x in arr:
        total += x
    return total

如何使用递归函数来完成这种任务呢?
**第一步:找出基线条件。**如果数组不包含任何元素或只包含一个元素,计算总和将非常容易。
在这里插入图片描述
第二步:每次递归调用都必须离空数组更近一步。
在这里插入图片描述
给函数sum传递的数组变的更短。换言之,这缩小了问题的规模!
在这里插入图片描述
编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。陷入困境时,请检查基线条件是不是这样的。

s u m sum sum函数代码:

def sum(list):
    if list == []:
       return 0
    return list[0] + sum(list[1:])

练习

编写一个递归函数来计算列表包含的元素数。

def count(list):
    if list == []:
       return 0
    return 1 + count(list[1:])

找出列表中最大的数字。

def max(list):
    if len(list) == 2:
       return list[0] if list[0] > list[1] else list[1]
    sub_max = max(list[1:])
       return list[0] if list[0] > sub_max else sub_max

4.2 快速排序

快速排序是一种常用的排序算法,比选择排序快得多。
使用快速排序对数组进行排序。对排序算法来说,最简单的数组什么样呢?基线条件为数组为空或只包含一个元素。在这种情况下,只需原样返回数组——根本就不用排序。
在这里插入图片描述
使用D&C,需要将数组分解,直到满足基线条件。下面介绍快速排序的工作原理。假设一个包含四个元素的数组如下:
在这里插入图片描述
首先,从数组中选择一个元素,这个元素被称为基准值(pivot)。接下来,找出比基准值小的元素以及比基准值大的元素。
在这里插入图片描述

这被称为分区(partitioning)。现在你有:

  • 一个由所有小于基准值的数字组成的子数组;
  • 基准值;
  • 一个由所有大于基准值的数组组成的子数组。

只要对这两个子数组进行快速排序,再合并结果,就能得到一个有序数组!
在这里插入图片描述
快速排序步骤如下:

  1. 选择基准值。
  2. 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。
  3. 对这两个子数组进行快速排序。

快速排序的代码如下:

def quicksort(array):
    if len(array) < 2:
       return array  #基线条件:为空或只包含一个元素的数组是“有序”的
    else:
       pivot = array[0] #递归条件
       less = [i for i in array[1:] if i <= pivot] #由所有小于基准值的元素组成的子数组
       greater = [i for i in array[1:] if i > pivot] #由所有大于基准值的元素组成的子数组
       return quicksort(less) + [pivot] + quicksort(greater)

4.3 再谈大O表示法

快速排序在最糟情况下,其运行时间为O( n 2 n^2 n2),与选择排序一样慢!但这是最糟情况。在平均情况下,快速排序的运行时间为O( n l o g n n log n nlogn)。还有一种名为合并排序(merge sort)的排序算法,其运行时间为O( n l o g n n log n nlogn)。
在这里插入图片描述

比较合并排序和快速排序

假设有下面这样打印列表中每个元素的简单函数。

def print_items(list):
    for item in list:
       print item

这个函数遍历列表中的每个元素并将其打印出来。它迭代整个列表一次,因此运行时间为O( n n n)。现在假设你对这个函数进行修改,使其在打印每个元素前都休眠1秒钟。

from time import sleep
def print_items2(list):
    for item in list:
        sleep(1)
        print item

它在打印每个元素前都暂停1秒钟。假设你使用这两个函数来打印一个包含5个元素的列表。

在这里插入图片描述
虽然使用大O表示法表示时,这两个函数的速度相同,但实际上print_items的速度更快。在大O表示法O( n n n)中, n n n实际上指的是这样的。

在这里插入图片描述
c c c是算法所需的固定时间量,被称为常量。大O表示通常不考虑这个常量,因为有时常量根本没有什么影响。参考简单查找和二分查找的大O表示:
在这里插入图片描述

但有时候,常量的影响可能很大,对快速查找和合并查找来说就是如此。实际上,快速查找的速度确实更快,因为相对于遇上最糟情况,它遇上平均情况的可能性要大得多。

平均情况和最糟情况

快速排序的性能高度依赖于你选择的基准值。假设你总是将第一个元素用作基准值,且要处理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。
在这里插入图片描述
现在假设你总是将中间的元素用作基准值,在这种情况下,调用栈如下。
在这里插入图片描述

调用栈短得多!因为你每次都将数组分成两半,所以不需要那么多递归调用。你很快就到达了基线条件,因此调用栈短得多。
第一个示例展示的是最糟情况,而第二个示例展示的是最佳情况。在最糟情况下,栈长为
O( n n n),有O( n n n)层,因此该算法的运行时间为O( n n n) * O( n n n) = O( n 2 n^2 n2)。而在最佳情况下,栈长为O( l o g n log n logn),层数为O( l o g n log n logn),因此整个算法需要的时间为O( n n n) * O( l o g n log n logn) = O( n l o g n n log n nlogn)。

最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O( n l o g n n log n nlogn)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值