【笔记】Data structure1

100 阅读3分钟

笔记主要结合了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);
        }

自己代码和范例的比较 我的思路和范例是一样的。

  1. 我最大的问题是多设置了一个数组,我的目的仅仅是判断数组中是否包含一个true,那么我只需要用一个变量才存储这个值即可,这样的话,可将空间复杂度改善为o(1)。

  2. 范例中for loop的条件不是i < nums.length-1, 而是i < nums.length,遍历到最后一项时会取undefined值,不过不影响代码逻辑。

  3. 我版本中的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中,范例没有返回值,这次为什么突然加上了返回值。