《算法图解》读书笔记

本文详细介绍了《算法图解》这本书的主要内容,从二分查找、运行时间和大O表示法开始,逐步深入到选择排序、递归、快速排序、散列表、广度优先搜索、迪克斯特拉算法、贪婪算法和动态规划等核心概念。通过实例解析,阐述了各种算法的原理、实现及应用场景,帮助读者理解算法在解决实际问题中的作用和性能评估。
摘要由CSDN通过智能技术生成

第一章 算法简介

二分查找

二分查找是一种算法,其输入必须是有序的元素列表。

对于包含n个元素的列表,使用二分查找最多需要$\log_2^n$

对数运算是幂运算的逆运算

log_2^n = a  →  2^a=n

在使用大O表示法讨论运行时间时,$\log$指的都是$\log_2$(以2为底)

二分法的JS实现:

// 二分法查找
const binarySearch = (list, target) => {
  let min = 0;
  let max = list.length - 1;
  while (min <= max) {
    const middle = Math.floor((min + max) / 2);
    if (list[middle] > target) {
      max = middle - 1;
    } else if (list[middle] < target) {
      min = middle + 1
    } else {
      return middle;
    }
  }
  return null
};

console.log(binarySearch([1, 3, 5, 7, 9], 3));

对于Python,如果取非整数索引,会自动向下取整,但是JS中不会,因为JS中的数组本质上是一个对象:

let a = ['x', 'y'];

// 等同于
{
  0: 'x',
  1: 'y',
  length: 2
}

所以如果为JS数组的非整数索引赋值,结果如下:

let a = [1, 2];
a[2.5] = 100;

console.log(a); 
// [1, 2, 2.5: 100]

运行时间

线性时间:最多需要查找的次数与列表长度相同$O(n)$

对数时间:最多需要查找的次数要$\log_2^n$, $O(\log n)$

$O(\log n)$$O(n)$快,当需要搜索的元素越多,前者比后者快得就越多

大O表示法

仅仅知道算法需要多长时间能运行完还不够,还需要知道运行时间如何随列表增长而增加。大O表示法表示的是算法运行时间的增速

大O表示法指出的是最糟糕的情况下的运行时间。

一些常见的大O运行时间有:

  • $O(\log n)$,对数时间,例如二分查找
  • $O(n)$,线性时间,例如简单查找
  • $O(n * \log n)$,例如快速排序,一种比较快的排序算法
  • $O(n^2)$,例如选择排序,一种比较慢的排序算法
  • $O(n!)$,例如旅行商问题的解决方案,一种非常慢的算法

image

算法的速度指的并非时间,而是操作数的增速(随着输入的增加,运行时间将以什么样的速度增加)。

第二章 选择排序

内存的工作原理

需要将数据存储到内存是,你请求计算机提供存储空间,计算机给你一个存储地址。需要存储多项数据时,有两种基本方式:数组和链表

数组

数组所分配的内存空间都是紧紧相连的。如果为数组添加新元素时,当前数组占用的内存满了,则需要将数组元素转移到其他地方。可以预留内存,但是都会带来两个缺点:

  1. 浪费内存
  2. 预留的内存用完后,还需要转移元素

数组的优点:需要随机的读取元素时,数组效率很高。

链表

链表中的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在了一起。

链表的优势在与插入元素方面,而且如果需要同时读取所有元素时,链表的效率很高。

在中间插入元素

使用链表插入元素很简单,只需要修改它前面的额按个元素指向的地址。使用数组插入元素时,则必须将后面的所有元素都后移。

在中间插入元素,链表是更好的选择。

删除元素

需要删除元素,链表也是更好的选择。

数组和链表的比较

数组用的更多,因为它支持随机访问,所以数组的读取速度更快。而链表只能顺序访问

数组和链表常见的操作的运行时间:

数组 链表
读取 $O(1)$ $O(n)$
插入 $O(n)$ $O(1)$
删除 $O(n)$ $O(1)$

Facebook存储用户信息的方法

Facebook存储用户信息时用的是链表数组,数组包含26个元素,每个元素指向一个链表。

image

插入元素时,对数组的移动最多26次,然后再从它指向的链表中进行插入。

读取元素时,可以直接找到数组中的对应元素,然后在链表中查找。

那么查找元素时,会不会效率太低?

选择排序

选择排序的原理就是每一轮在n个元素中进行查找,找出最小元素,放到结果当中。

每一轮的操作时间复杂度为$O(n)$,这样的操作需要执行n次,所以时间复杂度为$O(n^2)$

有一个问题,每一轮进行后,下一轮要检查的元素会逐渐减少,实际随后检查的元素格式是一个公差为1的等差数列n-1n-221,平均每次检查的元素是n/2,因此运行时间为$O(n * n/2)$,但是大O表示法会忽略诸如1/2这样的常数,所以为$O(n^2)$

选择排序的JS实现:(从小到大排序)

function chooseSort (arr) {
  for (let i = 0; i < arr.length; i++) {
    let minIndex = i;
    for (let j = i; j < arr.length; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j
      }
    }
    [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
  }
  return arr;
}

选择排序的速度不是很快,快速排序的速度更快,时间复杂度为$O(n * \log n)$

第三章 递归

递归是一种很多算法都使用的一种编程方法。

递归只是让解决方案更加清晰,并没有性能上的优势。实际上在有些情况下使用循环的性能更好。

基线条件和递归条件

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

栈是一种简单的数据结构,有两种操作:压入和弹出。

计算机内部使用被称为调用栈的栈。在调用过程中,调用另一个函数时,当前函数暂停并处于未完成状态。该函数的所有变量的值都保留在内存中。

在递归调用栈中,包含着未完成的函数调用,自己无需跟踪哪些函数还没有被执行,栈会完成这个步骤。

使用栈很方便,但是也有性能代价,存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,那么以为这计算机储存了大量函数调用的信息。

解决方法:

  1. 改用循环
  2. 使用尾递归(尾递归的实现需要确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数)

第四章 快速排序

分治法

分治法是解决问题的一种思路,一种递归式问题解决方法。

用分治法解决问题有包括两个步骤:

  1. 找出基线条件(即递归终止的条件),这种条件必须尽可能简单
  2. 不断将问题分解(或者说缩小规模),直到符合基线条件

一道题目:将一个长度为length、宽度为width的矩形均匀地分成方块,并确保分出的方块是最大的

分析这道题,首先要找出递归条件,最容易处理的情况是一条边的长度是另一条边的整数倍。这个时候就可以将矩形均匀地分成方块。

然后找出递归条件,每次递归都需要缩小问题规模。首先找出这块地可容纳的最大方块A,剩余的土地为B,根据欧几里得算法,适用于这小块地的最大方块,也是适用于整块地的最大的方块(就这个算法我就不知道,真做的时候就做出不来)。据此就可以不断的缩小规模,直到满足基线条件:

// 找到一个矩形可以均匀的划分的最大的小方块的尺寸
const getMaxSquare = (length, width) => {
  if (width > length) {
    [width, length] = [
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值