前言
-
本书仅为读者读书笔记,学习请阅读原书。书中算法实现基于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)
第四章 运用大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来优化代码
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忽略常数
-
对于不同分类,存在一临界点,在这一点之后,一类算法会快于另一类并保持下去,大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)
}
效率:
平均情况
在最坏的情况里,选择排序比插入排序快。但是我们还应该考虑平均情况。
现实世界里,最常出现的是平均情况。
插入排序效率
| 最坏 | 平均 | 最好 | |
|---|---|---|---|
| 选择排序 |
第七章 散列表
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)])
}
效率:
| 最好 | 平均 | 最坏 | |
|---|---|---|---|
| 插入排序 | |||
| 快速排序 |
很多编程语言自带的排序算法都基于快速排序
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 时间与空间的权衡
检查数组中是否存在重复元素
时间复杂度,空间复杂度
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
}
时间复杂度,空间复杂度
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
}