【读书笔记】数据结构与算法图解

224 阅读7分钟

前言

  • 本书仅为读者读书笔记,学习请阅读原书。书中算法实现基于Ruby,python,JavaScript三种语言,笔者一律使用JavaScript实现。

  • 本书是一本很好的数据结构与算法入门书籍,能让读者理解数据结构和算法核心概念。

  • 豆瓣:8.9

第一章 数据结构为何重要

编程基本上就是在和数据打交道

1.1 数组

了解某个数据结构的性能,得分析程序怎样操作这一数据结构

一般的数据结构都有以下四种操作

  • 读取 O(1)
  • 查找 O(N)
  • 插入 O(N)
  • 删除 O(N)

操作的速度不按时间计算,而是按步数计算

1.2 集合

一种不允许元素重复的数据结构

读取查找删除和数组一样,插入需要遍历检查重复(2N)。

第二章 算法为何重要

2.1 有序数组

二分查找

function binarySearch(arr, value) {
    var low_boundry = 0;
    var upper_boundry = arr.length - 1;
    var count = 0
    while(low_boundry <= upper_boundry) {
        count++
        let midIndex = Math.floor((upper_boundry + low_boundry) / 2);
        if (arr[midIndex] === value) {
            console.log(count)
            return true;
        }
        if (arr[midIndex] < value) {
            low_boundry = midIndex + 1
        } else {
            upper_boundry = midIndex - 1
        }
    }
    console.log(count)
    return false
}

有序数组并不是所有操作都比常规数组要快,插入就相对要慢,但是查找却快了很多。

世界上并没雨哪种适用于所有场景的数据结构和算法。

第三章 大O记法

大O不关心算法所用时间,只关注其所用步数。

大O回答的是:当数据增长时,步数如何变化。

若无特别说明,大O一般都指最坏的情况。

二分搜索的大O:O(logN)

所需最大步数: \frac{N}{2^x}\\
x = log(N)

第四章 运用大O来给代码提速

4.1冒泡排序
function bubbleSort(arr) {
    var i = arr.length
    while(i > 0) {
        for (let j = 0; j < i; j++) {
            if (arr[j] > arr[j+1]) {
                let temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
        i--;
    }
    console.log(arr)
}

效率:O(N^2)

第五章 用或不用大O来优化代码

5.1 选择排序
function selectSort(arr) {
    for (let i = 0; i < arr.length; i++) {
        let temp = arr[i]
        let startIndex = i
        let exchangeIndex = i
        for (let j = i; j < arr.length; j++) {
            if (arr[j] < temp) {
                exchangeIndex = j
                temp = arr[j]
            }
        }
        if (startIndex !== exchangeIndex) {
            let temp = arr[startIndex]
            arr[startIndex] = arr[exchangeIndex]
            arr[exchangeIndex] = temp
        }
    }
    console.log(arr);
}
  • 效率:O(N^2) (严格来说应该是O(\frac{N^2}{2})

  • 但是大O忽略常数

  • 对于不同分类,存在一临界点,在这一点之后,一类算法会快于另一类并保持下去,大O不关心临界点位置

第六章 乐观的调优

插入排序

function insertSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        let tempValue = arr[i]
        let index = i
        while (arr[index - 1] > tempValue && index > 0) {
            arr[index] = arr[index - 1]
            index--
        }
        arr[index] = tempValue
    }
    console.log(arr)
}

效率:O(N^2)

平均情况

在最坏的情况里,选择排序比插入排序快。但是我们还应该考虑平均情况。

现实世界里,最常出现的是平均情况。

插入排序效率

最坏 平均 最好
选择排序 O(N^2) O(\frac{N^2}{2}) O(N)

第七章 散列表

7.1探索散列表

散列表由一对对的数据组成(键值对形式)。

散列表解决冲突的一种经典做法是:分离链接。把值放到格子关联的数组中。

所以散列表的设计应该尽量减少冲突,以便查找都能以O(1)完成。(数组中查找是O(N))

7.2 找到平衡

散列表的效率取决于

  • 要存多少数据
  • 有多少可用格子
  • 用什么样的散列函数

数据量和格子数的比值称为负载因子,理想的负载因子是0.7。

散列表非常适合检查元素的存在性。

第八章 栈和队列

多了一些约束条件的数组

8.1 栈
  • 只能在末尾插入数据
  • 只能读取末尾的数据
  • 只能移除末尾的数据
8.2 队列
  • 只能在末尾插入数据
  • 只能读取开头的数据
  • 只能移除开头的数据

第九章 递归

9.1 用递归代替循环

几乎所有的循环都能够转换成递归,但能用不代表该用。

9.2 基准情形

在递归领域,不再递归的情形称为基准情形

9.3 计算机眼中的递归

不断将函数压入调用栈,如果是无限递归,就会导致栈溢出。

第十章 飞快的递归算法

10.1 快速排序
function quickSort(array) {
    if (array.length < 2) {
        return array
    }
    var pivot = array[array.length-1]
    var left = []
    var right = []
    for (let i = 0; i < array.length-1; i++) {
        if (array[i] <= pivot) {
            left.push(array[i])
        } else {
            right.push(array[i])
        }
    }
    return ([...quickSort(left), pivot, ...quickSort(right)])
}

效率:

最好 平均 最坏
插入排序 O(N) O(N^2) O(N^2)
快速排序 O(NlogN) O(NlogN) O(N^2)

很多编程语言自带的排序算法都基于快速排序

10.2 快速选择

如:查找数组中第二大的值

const arr = [4, 1, 2, 3, 7, 8, 10, 32, 43];

function quickSort(array, index) {
    if (array.length < 2) {
        return array[0]
    }
    var pivot = array[array.length-1]
    var left = []
    var right = []
    for (let i = 0; i < array.length-1; i++) {
        if (array[i] <= pivot) {
            left.push(array[i])
        } else {
            right.push(array[i])
        }
    }
    if (index < left.length) {
        return quickSort(left, index)
    } else if (index > left.length) {
        return quickSort(right, index)
    } else {
        return left[left.length - 1]
    }
}

console.log(quickSort(arr, 6))  // 8

结合二分和快速排序,可以在无序数组中很快找到某一排序的值。

第十一章 基于节点的数据结构

11.1 基础概念

存放数据的格子是不连续的,这种结构叫做结点。

每个结点除了保存数据,还保存着链表里的下一结点的内存地址。

读取:从结点头部开始查找,O(N)

查找:同数组,一个一个查找,O(N)

插入:尾插入O(1),首插入O(N)

删除:尾删除O(N),首删除O(1)

11.2 实用场景

删除电子邮件中无效的值,数组中每删除一次需要左移一次剩下的数组,以填补空隙;链表结构上删除只需要改动前面一个结点的指向就ok了。

11.3 双向链表

首尾插入删除效率都为O(1),适合作为队列的底层数据结构。

第十二章 二叉树

12.1 基本概念

二分查找的使用基础基于有序数组,但是有序数组的插入和删除是缓慢的,平均效率为O(N)。

树也是基于结点的数据结构。但是树里面每个结点可以含有多个链分别指向其他多个结点。

二叉树遵守以下规则:

  • 每个结点的子结点数量为0、1、2
  • 如果有两个子结点,其中一个子结点必须小于父结点,另一个子结点的值必须大于父结点。
12.2 效率

随意打乱的数据创建的二叉树才是比较平衡的,已排序的数据创建的二叉树会完全失衡

  • 查找:平衡二叉树为O(logN)
  • 插入:平衡二叉树为O(logN)
  • 删除:平衡二叉树为O(logN)
    • 如果没有子结点直接删除
    • 只有一个子结点,子结点填到被删除结点位置
    • 两个子结点,使用后继结点填到该位置
    • 后继结点有右子结点则填到后继结点位置
12.3 中序遍历

待补充...

第十三章 连接万物的图

13.1 基本概念

图是一种善于处理关系型数据的数据结构,可以轻松表示数据之间如何关联。

如人际关系,每个结点都是一个顶点。

图的实现形式很多,最简单的方法之一就是散列表。

图可以分为有向图(Facebook)和无向图(Twitter)。

13.2 图的广度优先搜索

待补充...

13.3 加权图

边上带有信息,可以是有向也可以是无向。

DIjkstra算法

待补充...

第十四章 空间限制

14.1 基本概念

时间复杂度关心运行的有多快。而空间复杂度关心消耗多少内存。

空间复杂度:当所处理的数据有N个元素时,该算法还需要额外消耗多少元素大小的内存空间。

实现小写单词数组转大写单词数组

var arr = ['apple', 'banana', 'orange'];

function arrToUpper(arr) {
    var newArr = [];
    for (let i = 0; i < arr.length; i++) {
        newArr.push(arr[i].toUpperCase())
    }
    return newArr
}

console.log(arrToUpper(arr)) // Array(3) ["APPLE", "BANANA", "ORANGE"]

接受一个N元素数组,产生另一个新的N元素数组,空间复杂度O(N)。

var arr = ['apple', 'banana', 'orange'];

function arrToUpper(arr) {
    for (let i = 0; i < arr.length; i++) {
        arr[i] = arr[i].toUpperCase()
    }
    return arr
}

console.log(arrToUpper(arr)) // Array(3) ["APPLE", "BANANA", "ORANGE"]

在原数组上操作,不消耗额外空间。空间复杂度O(1)

14.2 时间与空间的权衡

检查数组中是否存在重复元素

时间复杂度O(N^2),空间复杂度O(1)

function hasDuplicateValue_1(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] === arr[j]) {
                return true
            }
         }
    }
    return false
}

时间复杂度O(N),空间复杂度O(N)

function hasDuplicateValue_2(arr) {
    var existNumber = [];
    for (let i = 0; i < arr.length; i++) {
        if (existNumber[arr[i]] === undefined) {
            existNumber[arr[i]] = 1
        } else {
            return true
        }
    }
    return false
}