第一章 算法简介
一、二分查找
从一个有序的元素列表中查找元素;每次取中间数并判断与目标元素的大小,排除了一半的数据,再从剩下列表中不断与新的中间数比较,直到找到目标元素。
function numSearch(arr, target) {
let low = 0,
high = arr.length - 1,
mid, midNum
while (low <= high) {
mid = Math.ceil((low + high) / 2)
midNum = arr[mid]
if (target == midNum) {
return mid
} else if (target < midNum) {
high = mid - 1
} else {
low = mid + 1
}
}
return null
};
二、大 O 表示法
表示算法运行时间随列表增长的增速,以增速为角度度量而不是秒。例如,假设列表包含n个元素。简单查找需要检查每个元素,运行时间为O(n)(线性时间);而使用二分查找需要检查行log n次,运行时间为O(log n)(对数时间)。
大 O 表示法指出了最糟情况下的运行时间。
第二章 选择排序
一、数组和链表
- 数组:存储数据的内存空间是连续的,每个元素都具备索引,索引从0开始;(支持随机访问)
- 链表:每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起;(支持顺序访问)
顺序访问意味着从第一个元素开始逐个地读取元素,随机访问意味着可直接跳到第十个元素。
操作运行时间
数组 | 链表 | |
---|---|---|
读取 | O(1) | O(n) |
插入 | O(n) | O(1) |
删除 | O(n) | O(1) |
二、选择排序
循环遍历列表,找出最大的数,并将该数添加到一个新列表中,直到所有数都排列为止。运行时间为O(n*n)
function findSmallest(arr) { // 寻找数组中最小的数
let smallest = arr[0] // 存储最小的值
let smallest_index = 0 // 存储最小元素的索引
for (let i = 0; i < arr.length; i++) {
if (arr[i] < smallest) {
smallest = arr[i]
smallest_index = i
}
}
return smallest_index
}
function selectionSort(arr) {// 对数组进行排序
let newArr = [],smallest
for(let i = 0; i < arr.length; i++){
smallest = findSmallest(arr) // 找出数组中最小的元素,并将其加入到新数组中
newArr.push(arr[smallest])
arr.splice(smallest,1)
}
return newArr
}
第三章 递归
一、递归
递归就是函数调用自己,让解决方案更清晰。
编写递归函数时,必须告诉它何时停止递归。每个递归函数都有两部分:基线条件和递归条件。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。
// 递归
function countdown(i){
if(i<=0){// 基线条件
return
}else{// 递归条件
countdown(i-1)
}
}
二、栈
栈是一种简单的数据结构,具有入栈和出栈的操作,操作的都是最上面的元素,计算机在内部使用被称为调用栈的栈。 使用栈虽然很方便,但存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。
第四章 快速排序
一、分而治之
分而治之(divide and conquer,D&C)是一种著名的递归式问题解决方法,通过不断缩小问题的规模来解决问题。因为是递归式,因此也会包含基线条件和递归条件。
D&C并非可用于解决问题的算法,而是一种解决问题的思路。
以计算数组之和为例:
二、快速排序使用
快速排序使用D&C解决,基线条件为数组为空或只包含一个元素。
- 当数组为空或长度为1,只需原样返回数组——根本就不用排序;
- 当数组长度为2,判断两个元素的大小并交换位置;
- 当数组长度为3,将数组分解直到满足基线条件。
快速排序的工作原理:
- 从数组中选择一个元素,这个元素被称为基准值;
- 分区——找出比基准值小的元素以及比基准值大的元素,分成两个子数组和一个基准值;
- 采用递归对子数组进行快速排序;
- 合并结果,得到有序数组。
function quicksort(arr){
let len = arr.length
if(len<2){
return arr
}else{
let pivot = arr[0]
let less=[],greater=[]
for(let i=1;i<len;i++){
if(arr[i]<pivot){
less.push(arr[i])
}else{
greater.push(arr[i])
}
}
return quicksort(less).concat([pivot],quicksort(greater))
}
}
若快速排序在平均情况下的运行时间为O(n log n),在最糟情况下,其运行时间为O(n*n)。
平均情况和最糟情况
快速排序的性能高度依赖于你选择的基准值。
这个数组并没有被分成两半;相反,其中一个子数组始终为空,这导致调用栈非常长,属于最糟情况;运行时间为O(n*n)
这个数组每次都将数组分成两半,所以不需要那么多递归调用,很快就到达了基线条件,因此调用栈短得多,属于最佳情况;运行时间为O(log n*n)
最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(n log n)。
第五章 散列表
一、散列函数 && 散列表
散列函数:“将输入映射到数字”,将同样的输入映射到相同的索引,将不同的输入映射到不同的索引。散列函数知道数组有多大,只返回有效的索引。
散列表:结合使用散列函数和数组创建的一种数据结构,数组和链表都被直接映射到内存,但散列表更复杂,它使用散列函数来确定元素的存储位置。散列表由键和值组成。
Python提供的散列表实现为字典,你可使用函数dict来创建散列表;在JavaScript则可通过包含多个属性的对象来代表散列表。
二、冲突
冲突是指给两个键分配的位置相同;
处理冲突最简单的办法:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
而最理想的情况是,散列函数将键均匀地映射到散列表的不同位置,好的散列函数很少导致冲突。否则,如果散列表存储的链表很长,散列表的速度将急剧下降。
在平均情况下,散列表执行各种操作的时间都为O(1),即不管数组多大,从中获取一个元素所需的时间都是相同的。
三、填装因子
填装因子 = 散列表占位数 / 散列表位置总数,填装因子度量的是散列表中有多少位置是空的。
假设你要在散列表中存储100种商品的价格,而该散列表包含100个位置。那么在最佳情况下, 每个商品都将有自己的位置,这个散列表的填装因子为1。一般来说,一旦填装因子>0.7就需要调整散列表长度,通常将数组增长一倍。
需要使用函数hash将所有的元素都插入到这个新的散列表中,良好的散列函数让数组中的值呈均匀分布。而良好的散列函数的实现,可了解SHA函数。