笔记主要结合了mosh的课程和Complete Intro to Computer Science课程。
1. Big O 时间复杂度
Big O用来描述一个算法的效率表现,包括时间表现和空间表现,对应时间复杂度和空间复杂度。
把y=3x² + x + 1O想象成是输入值的数量x和所用时间y的函数,3x²对结果的影响是最大的,特别是在数字很大时,以至于我们可以忽略后面的+x+10。 Big O其实就是忽略那些对结果影响小的部分,而把注意力放在影响大的部分。对于3x² + x + 1O来说,它的Big O就是O(n²),也就是说当我们有n个值时,它需要花费的时间受n*n影响最大。
// 为了考虑它的时间效率,考虑我们要给cpu多少指令。
//(1)和(3)不受参数长度的影响,只执行一次,它们相当于上面+x+10部分
//(2)部分执行次数是参数长度的一次函数,
// 所以综合来看,这个算法的时间复杂度是O(n)
function crossAdd(input) {
var answer = []; // ======== (1)
for (var i = 0; i < input.length; i++) { // ======== (2)
var goingUp = input[i];
var goingDown = input[input.length - 1 - i];
answer.push(goingUp + goingDown);
}
return answer;// ======== (3)
}
```
下面这个算法中描述的是检查数据中是否存在自己想要的目标数据。无疑根据输入参数的不同,这个算法的执行时间是不同的。但在考虑算法的时间复杂度时,我们应该考虑的是最坏情况。而这里的最坏情况则是:needle是数组中最后一个数据,或者说数据中根本不存在这个数据。所以它的时间复杂度仍然是O(n)。
function find(needle, haystack) {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i] === needle) return true;
}
return false;
}
下面这个例子描述的是寻找一个数组中最中间的量(如果长度是偶数,则返回中间偏后的量)。 这个算法的时间不受输入量的影响,可以看作是一个类似于y=3这样的常量函数,此时它的时间复杂度写作O(1)
function getMiddleOfArray(array) {
return array[Math.floor(array.length / 2)];
}
一定要追求低时间复杂度吗?
回答是:看情况。例如要为论坛帖子做排序,如果网站流量很小,O(n²)的算法又简单易维护,那么O(n²)算法是一个不错的选择(不要在不重要的事情上浪费时间)。但如果是高流量的网站,例如Reddit,那么O(n²)可能会导致网站崩溃,此时应该去追求效率更好的算法。
2 空间复杂度
概念: 一个算法需要占据多少RAM或硬盘空间。
例如:假设一个算法对于参数数组中的每一个项目都会创建一个新数组,那么如果参数长度为10,它需要创建的数据是10个数组。所以它的空间复杂度是O(n)
在空间开销很重要的时候,要避免使用函数式编程。
3 bubble sort
思路: 数组项目依次进行遍历,如果前一项元素大于后一项,就交换二者位置,重复这个过程,直到没有需要交换的项。
我自己的代码
// 空间复杂度:需要一个额外的数组outOfOrder,所以是O(n).
// 时间复杂度:在最坏的情况下(数组倒序)下:
// 第一轮可以把最大的数字放到正确位置
// 第二轮可以把第二大的数字放到正确位置
// 一次类推
// 一共需要n-1轮才能把所有元素排序好,所以for loop内代码相当于要执行(n* (n-1))次
// 所以时间复杂度是O(n^2)
function bubbleSort(arr) {
// 用一个数组存储状态,以判断是否已经没有需要交换位置的元素
let outOfOrder = [true];
while (outOfOrder.includes(true)) {
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) {
outOfOrder[i] = true;
swap(arr, i, i + 1);
} else {
outOfOrder[i] = false;
}
}
}
return arr;
}
// **********
function swap(arr, a, b) {
let tmp = arr[a];
arr[a] = arr[b];
arr[b] = tmp;
}
// **********
let arr = [1, 3, 2, 99, 5, 3, 7, 3, 8];
console.log(bubbleSort(arr));
课上提供的范例:
function bubbleSort(nums) {
let swapped = false;
do {
swapped = false;
for (let i = 0; i < nums.length; i++) {
if (nums[i] > nums[i + 1]) {
const temp = nums[i];
nums[i] = nums[i + 1];
nums[i + 1] = temp;
swapped = true;
}
}
} while (swapped);
}
自己代码和范例的比较 我的思路和范例是一样的。
-
我最大的问题是多设置了一个数组,我的目的仅仅是判断数组中是否包含一个true,那么我只需要用一个变量才存储这个值即可,这样的话,可将空间复杂度改善为o(1)。
-
范例中for loop的条件不是i < nums.length-1, 而是i < nums.length,遍历到最后一项时会取undefined值,不过不影响代码逻辑。
-
我版本中的return 操作无必要,因为数组在过程中已经被改变了。
4 insertion sort
insertion是插入的意思,这个算法的基本思路是,从第二项起遍历,将每一项插入到前面部分的正确位置。所以,当遍历操作完第二项时,可以保证前两项的顺序是正确,而操作完第3项时,可以保证前三项的顺序是正确的...操作完最后一项,数组就被顺利排序了。
我的版本:
// 空间复杂度:O(1)
// 时间复杂度:O(n^3)(3个loop)
function insertionSort(arr) {
for (let i = 1; i < arr.length; i++) {
for (let j = 0; j < i; j++) {
if (arr[i] < arr[j]) {
insert(arr, i, j);
}
}
}
}
// *********
// 将destIndex到index的部分数组元素整体后移,并将index位置上的元素插入到destIndex位置上。
function insert(arr, index, destIndex) {
const tmp = arr[index];
for (let i = index; i > destIndex; i--) {
arr[i] = arr[i - 1];
}
arr[destIndex] = tmp;
}
// ****
let arr = [3, 62, 7, 28, 199, 144, 0, 2, -4];
insertionSort(arr);
console.log(arr);
范例版本:
function insertionSort(nums) {
for (let i = 1; i < nums.length; i++) {
let numberToInsert = nums[i]; // the numberToInsert number we're looking to insert
let j; // the inner counter
// loop from the right to the left
for (j = i - 1; nums[j] > numberToInsert && j >= 0; j--) {
// move numbers to the right until we find where to insert
nums[j + 1] = nums[j];
}
// do the insertion
nums[j + 1] = numberToInsert;
}
return nums;
}
我的版本与范例版本的比较
先简单看了一下范例版本,发现:我的版本中,我是从左往右寻找插入位置,范例版本中,数组元素shift和比较的过程是同时进行的,而我的版本是分开进行的,导致时间复杂度增加(我在一开始分析自己的时间复杂度时没注意到这点。)
先不详细观察范例,先根据上面发现的问题以我的方式再改写一下代码
function insertionSort(arr) {
for (let i = 1; i < arr.length; i++) {
// 这里声明curr,因为之后移元素的话,curr会被覆盖
let curr = arr[i];
// 这里声明j是为了记忆插入位置,如果在for循环内部插入会更复杂。
let j;
inner: for (j = i - 1; j >= 0; j--) {
// 如果元素比插入项大,将元素后移(shift)
if (arr[j] > curr) { // 注意这里是curr,而不是arr[i]
arr[j + 1] = arr[j];
} else {
break inner; //如果元素不再比curr大,则停止该循环,记忆插入位置。
}
}
arr[j + 1] = curr;
console.log(arr);
}
}
再次比较: 我内部的for循环写得明显没有范例版本简洁,可以学习一下这种写法。 疑惑的点..上次bubble sort中,范例没有返回值,这次为什么突然加上了返回值。