5

以JavaScript语言为主,学习数据结构与算法。

算法需要依靠数据结构计算.

什么是算法
  1. 一个有限指令集
  2. 接受一些输入(有些时候不需要输入)
  3. 产生输出
  4. 一定在有限步骤之后终止
  5. 每一条指令必须
时间复杂度Tn

根据算法写成的程序在执行时占用存储单源的长度

空间复杂度Sn

根据算法写成的程序在执行时好费时间的长度

数据结构
  • 栈:一种遵从先进后出 (LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
  • 队列:一种遵循先进先出 (FIFO / First In First Out) 原则的一组有序的项;队列在尾部添加新元素,并从头部移除元素。最新添加的元素必须排在队列的末尾。
  • 链表:存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的;每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针/链接)组成。
  • 集合:由一组无序且唯一(即不能重复)的项组成;这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。
  • 字典:以 [键,值] 对为数据形态的数据结构,其中键名用来查询特定元素,类似于 Javascript 中的Object
  • 散列:根据关键码值(Key value)直接进行访问的数据结构;它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度;这个映射函数叫做散列函数,存放记录的数组叫做散列表。
  • 树:由 n(n>=1)个有限节点组成一个具有层次关系的集合;把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的,基本呈一对多关系,树也可以看做是图的特殊形式。
  • 图:图是网络结构的抽象模型;图是一组由边连接的节点(顶点);任何二元关系都可以用图来表示,常见的比如:道路图、关系图,呈多对多关系。

数据结构

栈是一种遵从先进后出 (LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。

栈也被用在编程语言的编译器和内存中保存变变量方法调用

class Stack {

    constructor() {
        this.items = []
    }

    // 入栈
    push(element) {
         this.items.push(element)
    }

    // 出栈
    pop() {
        return this.items.pop()
    }

    // 末位
    get peek() {
        return this.items[this.items.length - 1]
    }

    // 是否为空栈
    get isEmpty() {
        return !this.items.length
    }

    // 长度
    get size() {
        return this.items.length
    }

    // 清空栈
    clear() {
        this.items = []
    }

    // 打印栈数据
    print() {
        console.log(this.items.toString())
    }
}

队列

队列是一种遵循先进先出 (FIFO / First In First Out) 原则的一组有序的项;队列在尾部添加新元素,并从头部移除元素。最新添加的元素必须排在队列的末尾。

前面的人优先完成自己的事务,完成之后,下一个人才能继续。

class Queue {

    constructor(items) {
        this.items = items || []
    }

    enqueue(element) {
        this.items.push(element)
    }

    dequeue() {
        return this.items.shift()
    }

    front() {
        return this.items[0]
    }

    clear() {
        this.items = []
    }

    get size() {
        return this.items.length
    }

    get isEmpty() {
        return !this.items.length
    }

    print() {
        console.log(this.items.toString())
    }
}

数组

数组: 相同数据类型的元素按一定顺序排列的集合

数组创建方式

// C++
int array[] = {5, 3, 2, 5, 6, 0, 10}

// JS
[]
new Array();

特性:存储连续的内存单元

好处:

  1. 省内存空间
  2. 规律整齐,获取快速,随机访问

为什么需要有链表:
移动和插入,删除,需要耗费性能和时间,就是链表产生的原因

类型检测

typeof

用来检测数据类型的运算符

typeof true; // boolean

返回都是一个字符串,其次字符串中包含了对应的数据类型。

返回的类型:

  1. string
  2. boolean
  3. number
  4. object
  5. function
  6. undefined

局限性:

  1. typeof null // "object" // null 是空对象指针
  2. typeof 不能具体区分是数组,还是正则,还是对象,还是时间对象. 引用类型的值,返回的是
console.log(typeof typeof typeof function() {});  // typeof function() {} --> 'function',  typeof 'function' --> 'string' ,  typeof 'string'  --> 'string'

if (typeof num2 == 'undefined') {
     num2 = 0;
}

num2 = num2 || 0; // 这种形式,和上面的默认值形式并不完全相同

typeof callback === 'function' ? callback() : null;

callback && callback();
instanceof

检测某一个实例是否属于某个类

弥补typeof不能判断引用类型。
特性:只要在当前实例的原型链上,都可以查询得到,检测出来的结果都是true

var obj = [123, 123];
console.log(obj instanceof Array); // true
console.log(obj instanceof RegExp); // false

instanceof很多的缺陷:

第一个缺陷:

对于基本数据类型,字面量创建出来的结果检测都为false。
从严格意义上讲,只有实例创建出来的结果才是标准的对象数据类型值。

1 instanceof Number // false
new Number(1) instanceof Number // true
true instanceof Boolean // false
'' instanceof String // false


console.log(1 instanceof Number);
console.log(new Number(1) instanceof Number);
// instanceof 的局限性,对于基本数据类型来说字面量方式创建出来的结果和实例方式创建出来的结果是有一定区别的,
// 从严格意义上来讲,只有实例创建出来的结果才是标准的对象数据类型值,也是标准的Number这个类的一个实例;对于字面量方式创建出来的结果
// 是基本数据类型值,不是严谨的实例,但是由于JS的松散特点,导致了可以使用Number.prototype上提供的方法。

第二个缺陷:

特征:
只要在当前实例的原型链上,都可以查询得到,检测出来的结果都是true

oDiv instanceof EventTarget  // true
oDiv instanceof Node // true

缺点:不确定性

// 在类的原型继承中,最后检测出来的结果未必正确
function Fn() {}
Fn.prototype = new Array(); // 原型继承
var f = new Fn(); 
console.log(f instanceof Function); // false
console.log(f instanceof Array); // true
// f->Fn.prototype -> Array.prototype -> Object.prototype

利用这个特性,可以创建类数组(索引和length),可以使用数组的方法(在它的原型链上就应该有Array.prototype了)

function arg() {
    // 存放私有属性
    this.index = 0;
    this.legnth = 0;
}
arg.prototype = Array.prototype;
constructor

构造函数,作用和instanceof非常的相似

可以处理基本数据类型

var num = 10;
console.log(num.constructor === Number); // true

construtor检测Object和instanceof不一样,一般情况下是检测不了的。

var reg = /\d+/;
console.log(reg.constructor === RegExp); // true
console.log(reg.constructor === Object); // false

局限性:
把类的原型进行重新,在重新的过程中,很有可能出现,把之前的constructor覆盖,这样检测出来的结果不准确。(原型继承)

对于特殊的数据类型nullundefined,它们的所属类是NullUndefined,但是浏览器把这两个类保护起来,不允许用户使用。

Object.prototype.toString.call();

Object 浏览器内置类,所有对象数据的基类。

类型检测最为准确.
首先获取Object原型上的toString方法,让方法执行,并且改变方法中的this关键词的指向.
Object.prototype.toString它的作用是返回当前方法的执行主体(方法中的this)所属类的详细信息

toString的理解:
字面意思是转化为字符串,但是某些toString方法不仅仅是转换为字符串。

console.log((1).toString()); // '1' // 使用的是:Number.prototype.toString();  // Number上的有toString()参数可以有进制转换
console.log((1).__proto__.__proto__.toString()); // '[object Object]'  // 使用的是:Object.prototype.toString(); 

对于Number,Boolean,String,Date,RegExp,Array,Function原型上的toString都是把当前的数据类型转换为字符串类型(它们的作用仅仅使用转换为字符串的)

Object.prototype.toString.call();并不是用来转换为字符串,而是一种形式[object Object]的格式的字符串

console.log(({}).toString()); // [object Object]
console.log(Math.toString());  // [object Math] 
console.log(Object.prototype.toString.call([])); // [object Array]

返回当前主体的类的属于信息

var obj = {};
console.log(obj.toString()); // toString中的this是谁的obj,返回的是obj所属类的信息 --> [当前实例是那种数据类型(这个是固定死的,所有数据类型都是对象类型) 当前主体的所属类]

算法概念

排序算法

  • 冒泡排序:比较任何两个相邻的项,如果第一个比第二个大,则交换它们;元素项向上移动至正确的顺序,好似气泡上升至表面一般。
  • 选择排序:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,以此循环,直至排序完毕。
  • 插入排序:将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,此算法适用于少量数据的排序,时间复杂度为 O(n^2)。
  • 归并排序:将原始序列切分成较小的序列,只到每个小序列无法再切分,然后执行合并,即将小序列归并成大的序列,合并过程进行比较排序,只到最后只有一个排序完毕的大序列,时间复杂度为 O(n log n)。
  • 快速排序:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行上述递归排序,以此达到整个数据变成有序序列,时间复杂度为 O(n log n)。
冒泡排序
var arr = [1, 2, 29, 12, 12, 19, 230, 120, 22]

function bubble_sort (arr) {
    var n = arr.length
    for (var i = 0; i < n - 1; i++) {
        for (var j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j+1]) {
                var tmp = arr[j]
                arr[j] = arr[j+1]
                arr[j+1] = tmp
            }
        }
    }
}

console.log(arr)
bubble_sort(arr)
console.log(arr)
选择排序
var arr = [1, 2, 29, 12, 12, 19, 230, 120, 22]

function select_sort (arr) {
    var n = arr.length
    for (var j = 0; j < n-1; j++) {
       var min_index = j
        for (var i = j+1; i < n; i++) { //  1 ~ n-1 时间复杂度:1-n, 2-n, 3-n
            if (arr[min_index] > arr[i]) {
                min_index = i
            }
        }
        var tmp = arr[min_index]
        arr[min_index] = arr[j]
        arr[j] = tmp 
    }
}

console.log(arr)
select_sort(arr)
console.log(arr)
快速排序

function query_sort (arr, first, last) {
    if (first >= last) return

    mid_value = arr[first]

    low = first
    high = last

    while (low < high) {
        while (low < high && arr[high] >= mid_value) {
            high -= 1
        }
        arr[low] = arr[high]
        while (low < high && arr[low] < mid_value) {
            low += 1
        }
        arr[high] = arr[low]
    }
    arr[low] = mid_value

    query_sort(arr, first, low-1)
    query_sort(arr, low+1, last)
}

li = [54, 26, 93, 17, 77, 34]
console.log(li)
query_sort(li, 0, li.length-1)
console.log(li)

搜索算法

  • 顺序搜索:让目标元素与列表中的每一个元素逐个比较,直到找出与给定元素相同的元素为止,缺点是效率低下。
  • 二分搜索:在一个有序列表,以中间值为基准拆分为两个子列表,拿目标元素与中间值作比较从而再在目标的子列表中递归此方法,直至找到目标元素。

其它

  • 贪心算法:在对问题求解时,不考虑全局,总是做出局部最优解的方法。
  • 动态规划:在对问题求解时,由以求出的局部最优解来推导全局最优解。
  • 复杂度概念:一个方法在执行的整个生命周期,所需要占用的资源,主要包括:时间资源、空间资源。

链表

链表: 一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本省的节点和一个指向下一个元素的引用(也称指针或链接)组成。

图片描述

链表优点:
添加或移动元素的时候不需要移动其它元素。
链表缺点:
链表需要使用指针,因此实现链表时需要额外注意。数组的另一个细节是可以直接访问任何位置的任何元素,而要想范文链表中间的一个元素,需要从起点(表头)开始迭代列表知道找到所需的元素。

 C++ 
struct node {
    int payload;
    node* next;
}

// JS
{}
new Object();

开辟内存,里面存放key 和value, 通过指针指向下一个元素。是无序的,通过指针来寻找下一个位置。

解决插入和删除的问题,通过指针直接指向某个一位置,寻找到该位置,插入或删除。

对于链表来说,拿第一个元素和拿第一亿零一个元素使用的时间是不同的,随机访问效率低下。
链表会指针每次判断,直到寻找到位置相同。

链表,和数组为基础,可以表现成或变形:
队列,hash表
栈:只能加在头部
队列: 一头近一头出
图:表现为连接的形式,每一个节点保存一堆的指针。 也可以通过链接矩阵的方式保存

hash表需要随机访问,第一层使用的是数组,第二层使用的是链表.

时间复杂度O(n)表示程序运行时间跟n有关,并且是线性关系。
空间复杂度O(1),表示所需空间为常量,并且与n无关。

// JavaScript链表

function LinkedList() {

  var Node = function(element) { // 辅助类
    this.element = element; // 添加到列表的值
    this.next = null; // 下一个节点项的指针
  }

  var length = 0; // 存储列表项的数量
  var head = null; // 存储第一个节点的引用

  // 向列表尾部添加一个新的项
  // 列表最后一个节点的下一个元素始终是 null
  this.append = function (element) {
    // 思路:
    // 1. 列表为空,添加的是第一个元素
    // 2. 列表不为空,向其追加元素

    var node = new Node(element), current;
    
    if (head == null) { // 列表中第一个节点
      head = node;
    } else {

      current = head;
      // 循环列表,直到找到最后一项  
      while(current.next) {
        current = current.next;
      }
      // 找到最后一项,将其next赋为node,建立链接
      current.next = node;
    }
    // 更新列表的长度
    length++;
  }

  // 向列表的特定位置插入一个新的项
  this.insert = function(position, element) {
    // 检查越界值
    if (position >= 0 && position <= length) {
      var node = new Node(element), previous, index, current = head;
      // 在第一个位置添加
      if (position == 0) {
        node.next = current;
        head = node;
      } else {
        while(index++ < position) {
          previous = current;
          current = current.next;
        }
        node.next = current;
        previous.next = node;
      }
      length++; // 更新列表长度
      return true;
    } else {
      return false;
    }
  }
  
  // 从列表中移除一项
  this.remove = function(element) {
    var index = this.indexOf(element);
    return this.removeAt(index);
  }

  // 从列表的特定位置移除一项
  // 第一种是从特定位置移除一个元素,第二种是根据元素的值移除元素

  // 给定位置移除一个元素
  this.removeAt = function(position) {
    // 思路: 1.  移除第一个元素
    // 2. 移除第一个以外的任一元素

    // 检查越界
    if (position > -1 && position < length) {
      var current = head, previous, index = 0; // current作用:移除元素的引用 // previous 作用:移除元素的前一个元素的引用

      // 移除第一项
      if (position == 0) { // 移除方法,堆中不引用地址,GC回收机制自动回收.
        head = current.next; // head 指向列表的第二个元素。 效果:current变量就是对列表中第一个元素的引用。如果把head赋为current.next,就会移除第一个元素。
      } else {
        while(index++ < position) {
          previous = current;
          current = current.next;
        }
        // 将previous与current的下一项链接起来:跳过current,从而移除它
        previous.next = current.next;
      }
      length--;
      return current.element;
    } else {
      return null;
    }
  }

  // 返回元素在列表中的索引。如果列表中没有该元素则返回 -1
  this.indexOf = function(element) {
    var current = head, index = -1;
    while(current) {
      if (element == current.element) {
        return index;
      }
      index++;
      current = current.next;
    }
    return -1;
  }

  // 如果链表中不包含任何元素,返回 true ,如果链表长度大于0则返回 false
  this.isEmpty = function() {
    return length === 0;
  }

  // 返回链表包含的元素个数。
  this.size = function() {
    return length;
  }

  // 由于列表项使用了 Node 类,就需要重写继承自JavaScript对象默认的toString 方法,让其只输出元素的值。
  this.toString = function() {
    var current = head, string = '';
    while(current) {
      string = current.element;
      current = current.next;
    }
    return stirng;
  }
  
  this.getHead = function() {
    return head;
  }

}

var list = new LinkedList();
list.append(15);
list.append(10);

数组反转

通过数组反转,推导出大O表达式.

// 思路:
// 1. 第一个和最后一个换
// 2. 第二个和倒二个换
// 3. 不断往中间逼进
// 4. 奇数中间一个不动,完成反转;偶数完成反转

var arr = [12, 34, 5, 3, 34, 22, 4];

function reverse(arr) {
  let left = 0; 
  let right = arr.length - 1;
  while(left < right) { // 循环给个终止条件, 左边不小于右边,跳出循环
    // 位置置换
    let tmp = arr[left];
    arr[left] = arr[right];
    arr[right] = tmp;
    left++;
    right--;
  }
}

看执行的时候,需要分析指令,每条指令执行都需要时间,并不是所有指令都根据数据量有关系。有的指令只执行一遍, 例如let left = 0;,let right = arr.length - 1;,不管数组再长,再大,这两条指令执行的时间不会变化。

clipboard.png

大O表达式:O(n) <= C1 * n + C2;

大O符号 :在计算机科学中,它在分析算法复杂性的方面非常有用。

斐波那契数列

斐波那契数列的递归实现

在数学上,菲波那切数列是以递归的方法来定义:
从第3项开始,每一项都等于前两项之和

  1. F0 = 0;
  2. F1 = 1;
  3. Fn = F(n-1) + F(n-2); (n>=2)
// JavaScript 实现 菲波那切数列算法 , 递归算法
/**
  * 斐波那契数列
  * 
  * 递归算法和如何计算时间复杂度
  */    
function fib(n) { // 通过自然归纳法 递归算法 O(2^n)   // <= c0*n + c1;   c0*2^n + c1
  // console.log(n);  // n = 0, n=1的时候 为 O(1), //T(n-1) , T(n) = O(2^n-1) + O(2^n-2) + O(1) = 1.5*O(2^n-1) + O(1)  <= 2*O(2^n-1) + O(1)  <= O(2^n-1) 
  if (n < 2) {
    return n;
  } else {
    return fib(n-1) + fib(n-2);  // T(n) = T(n-1) + T(n-2) + O(1);
  }
}

for (let i=0; i<50; i++) {
  console.log(fib(i));
}

递归算法计算时间复杂度
通过自然归纳法计算时间复杂度

递归算法在菲波那切数列中应用时间复杂度很高,从内存的角度看,每次开辟一个新的内存空间,一直没有被销毁回收,占用巨大内存。
时间复杂度为:O(2*n)

clipboard.png

// JavaScirpt 迭代方式实现  菲波那切数列算法
function fibonacci(n) {
    var f = [];
    f[1] = 1;
    f[2] = 1;

    for (var i=3; i<n; i++) {
        f[i] = f[i-1] + f[i-2];
    }
    return f;
}

约瑟夫环问题

差数淘汰:
已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。

单向链表: 第一个单元,第二个单元... 最后一个接地.

循环链表: 第一个单元,第二个单元... 最后一个链表接回到第一个链表单元.

// reset(), current(), next(), prev(), search(), end() 

Array.prototype.pointer = 0; // 模拟数组内部指针

// reset 函数,0将数组内部指针归为,(指向第一个元素)
var reset = function (arrayObj) {
  if (!(arrayObj instanceof Array)) {
    console.warn('reset function arguments typeof error');
    return;
  }
  // 重置指针
  arrayObj.pointer = 0;
}

// current 函数,返回数组内部指针的当前元素
var current = function (arrayObj) {
  if (!(arrayObj instanceof Array)) {
    console.warn('current function arguments typeof error');
    return;
  }
  return arrayObj[arrayObj.pointer];
}

// end 函数,将数组内部指针指向最后一个元素,并返回最后一个元素的当前位置
var end = function (arrayObj) {
  if (!(arrayObj instanceof Array)) {
    console.warn('end function arguments typeof error');
    return;
  }
  arrayObj.pointer = arrayObj.length - 1;
  return arrayObj[arrayObj.pointer];
}

// next函数,将数组内部指针下移一位,如果已经指向最后一个元素则返回false
var next = function (arrayObj) {
  if (!(arrayObj instanceof Array)) {
    console.warn('next function arguments typeof error');
    return;
  }
  // 指针后移
  arrayObj.pointer++;
  // 判断指针是否在最后一个
  if (typeof arrayObj[arrayObj.pointer] == 'undefined') {
    arrayObj.pointer--; // 重置回最后一个
    return false;
  }
  return true;
}

// prev函数,将数组内部指针上移一位,如果已经指向第一个元素则返回false
var prev = function (arrayObj) {
  if (!(arrayObj instanceof Array)) {
    console.warn('prev function arguments error');
    return;
  }
  // 指针前移
  arrayObj.pointer--;
  // 判断指针是否第一个
  if (typeof arrayObj[arrayObj.pointer] == 'undefind') {
    arrayObj.pointer++; // 重置回第一个
    return false;
  }
  return arrayObj[arrayObj.pointer];
}

// unset 函数, 删除指定的数组元素
var unset = function (idx, arrayObj) {
  if (!(arrayObj instanceof Array)) {
    console.warn('unset function arguments error');
    return;
  }
  if (typeof arrayObj[idx] == 'undefined') {
    console.warn("unset() 函数参数 idx 错误!不存在此元素!");
    return false;
  }
  arrayObj.splice(idx, 1);
  return true;
}

// search 函数,通过数组键值返回数组键名
var search = function (value, arrayObj) {
  if (!(arrayObj instanceof Array)) {
    console.warn('search function arguments  error');
    return;
  }
  for (var i in arrayObj) {
    if (arrayObj[i] == value) {
      return i; // 返回键名
    }
  }
  return false;
}

// getKingMonkey 主函数 // n 只猴子,数到 m   // 4个人数,到3停止一次。
function getKingMonkey(n, m) {
  debugger;
  // 1. 构造元素
  // 2. 循环

  a = new Array();
  for (var i = 1; i <= n; i++) {
    a[i] = i;
  }
  a[0] = 0;  // 补第0个位置 // [undefined × 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] --> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  unset(0, a); // 删除第0个元素  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  reset(a); // 数组内部指针归为0 

  while (a.length > 1) {
    for (counter = 1; counter <= m; counter++) {
      if (next(a)) { // 是否具有后续指针
        if (counter == m) { 
          unset(search(prev(a), a), a);
        }
      } else {
        reset(a); // 重置指针到第0位
        if (counter == m) {
          unset(search(end(a), a), a);
          reset(a); // 重置指针到第0位
        }
      }
    }
  }
  return current(a);
}

console.log(getKingMonkey(4, 3)); // 1

对于代码无法去看时间复杂度,一般从逻辑上考虑。

function getKingMonkey(n, m) {
  debugger;
  // 1. 构造元素
  // 2. 循环

  a = new Array();
  for (var i = 1; i <= n; i++) {
    a[i] = i;
  }
  a[0] = 0;  // 补第0个位置 // [undefined × 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] --> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  unset(0, a); // 删除第0个元素  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  reset(a); // 数组内部指针归为0 

  while (a.length > 1) {
    for (counter = 1; counter <= m; counter++) {
      if (next(a)) { // 是否具有后续指针
        if (counter == m) { 
          unset(search(prev(a), a), a);
        }
      } else {
        reset(a); // 重置指针到第0位
        if (counter == m) {
          unset(search(end(a), a), a);
          reset(a); // 重置指针到第0位
        }
      }
    }
  }
  return current(a);
}

假设n次
主函数的getKingMonkey中的外层while循环会执行n-1次, for循环会执行 k-1 次, 加上其它固定执行命令代码时间。 总体为:(n-1) * (k-1) + c

(n-1) * (k-1) + c ≈ nk - C0*n - C1*k + c ≈ nk - C0*n - C1*k 

时间复杂度:O(nk)
n可能很大,k可能很大

合并链表

合并两个已经排序好的链表

// C++ 代码实现
#include <iostream>

struct node{
  int payload;
  node* next;
  node(int payload) {this->payload = payload; next = nullptr;};
};

class linkedlist{
  node *head, *tail;
public:
  //constructor, initialize head and tail to nullptr
  linkedlist() : head(nullptr),tail(nullptr){};
  //push a new node at the end of the list
  void push_back(int value){
    if(empty()){ 
      head = tail = new node(value);
    }else{      
      tail->next = new node(value);
      tail = tail->next;
    }
  }
  //return the value stored in the first node
  int front(){
    if(empty()){
      throw "The list is empty.";
    }
    return head->payload;
  }
  //remove the first node
  void pop_front(){
    if(empty()){
      throw "The list is empty.";
    }
    node* first_node = head;
    head = head->next;
    delete first_node; 
  }
  bool empty(){
    return head == nullptr;
  }
  void output(){
    node* iterator = head;
    while(iterator){
      std::cout << iterator->payload << " ";
      iterator = iterator->next;
    }
    std::cout << std::endl;
  }
  
};

//merge two sorted linked list, return a new list
linkedlist merge(linkedlist a, linkedlist b){
  linkedlist result;
  while(!a.empty() || !b.empty()){ // 考虑极端情况, a 链表为空,把b链表中的一个个添加 结果链表中; b 链表为空,把a链表中的一个个添加 结果链表中
    if(a.empty()){
      result.push_back(b.front());
      b.pop_front();
    }else if(b.empty()){
      result.push_back(a.front());
      a.pop_front();
    }else if(a.front() > b.front()){ // a,b 链表都不为空, 看a和b 的head 谁大 (比较值的大小)。 a > b , b添加到结果链表中
      result.push_back(b.front());
      b.pop_front();
    }else{
      result.push_back(a.front()); // a < b , a添加到结果链表中
      a.pop_front();
    }
  }
  return result;
}
int main(){

  linkedlist a,b;
  
  linkedlist result = merge(a, b);
  result.output();
  return 0;
}

时间复杂度计算:

linkedlist merge(linkedlist a, linkedlist b){
  linkedlist result;
  while(!a.empty() || !b.empty()){ // 考虑极端情况, a 链表为空,把b链表中的一个个添加 结果链表中; b 链表为空,把a链表中的一个个添加 结果链表中
    if(a.empty()){
      result.push_back(b.front());
      b.pop_front();
    }else if(b.empty()){
      result.push_back(a.front());
      a.pop_front();
    }else if(a.front() > b.front()){ // a,b 链表都不为空, 看a和b 的head 谁大 (比较值的大小)。 a > b , b添加到结果链表中
      result.push_back(b.front());
      b.pop_front();
    }else{
      result.push_back(a.front()); // a < b , a添加到结果链表中
      a.pop_front();
    }
  }
  return result;
}

逻辑上的循环,并不知道具体会循环多少次,去看关键逻辑

假设:a的长度是m,b的长度是n。
总共循环了m+n次。

(m+n) * O(1)

时间复杂度为:
O(m+n)

归并排序

杂乱无章的序列,可以从小到大,可以从大到小,处理之后返回.
归并排序是比较次数最少的一种排序
二分法 时间复杂度: O(m+n)
二分法 使用递归方式也称之为 自顶向下的算法
二分法 使用普通循环的方式排序,也称之为 自低向上的算法

递归的第一种方式:
数组的位置二分法排序:

init();
var array;
var left, right;
function init() {
  array = [3, 4, 2, 1, 7, 5, 8, 9, 0, 6];
  left = 0;
  right = array.length - 1;

  mergeSort(array, left, right);
}

function mergeSort(array, left, right) {
  if (left >= right) return;
  var middle = left + parseInt((right - left) / 2);
  mergeSort(array, left, middle);
  mergeSort(array, middle + 1, right);
  merge(array, left, middle, right);
}

function merge(array, left, middle, right) {
  var i = left, j = middle + 1;
  var aux = [];
  for (var k = left; k <= right; k++) {
    aux[k] = array[k];
  }
  for (var a = left; a <= right; a++) {
    if (i > middle) {
      array[a] = aux[j++];
    } else if (j > right) {
      array[a] = aux[i++];
    } else if (parseInt(aux[i]) < parseInt(aux[j])) {
      array[a] = aux[i++];
    } else {
      array[a] = aux[j++];
    }
  }
}

console.log(array);

递归的第二种方式

var arr = [3, 4, 2, 1, 7, 5, 8, 9, 0, 6];

function mergeSort(arr) {
  var len = arr.length;
  var left, right, middle = Math.floor(arr.length / 2); // (left + right) / 2
  if (len <= 1) {
    return arr;
  }
  left = arr.slice(0, middle); // 得到下标从0~middle-1的数组
  right = arr.slice(middle); // 得到下标从index开始到末尾的数组
  return merge(mergeSort(left), mergeSort(right)); // 递归
}

function merge(left, right) {
  // 该函数与快排类似,每次left或者right都是要shift掉第一个元素,表示left或者right是会变化的,最后arr.concat,
  // 因为不知道left或者right其中一个哪个剩下元素,所以要将剩下的元素给加上
  var arr = [];
  while (left.length && right.length) {
    if (left[0] < right[0]) {
      arr.push(left.shift());
    } else {
      arr.push(right.shift());
    }
  }
  return arr.concat(left, right);
}

console.log(mergeSort(arr));

普通循环实现归并排序
思路:
从第一项和第二项合并并排序,第三和第四合并并排序,一直循环反复.
新出来的项的序列。
从第一项和第二项合并并排序,循环反复。
循环反复.

5, 2, 1, 4, 3

↓
2,5 
1,4
3

↓

1,2,4,5
3,

↓

1,2,3,4,5

实现代码:

function isArray1(arr) {
  if (Object.prototype.toString.call(arr) == '[object Array]') {
    return true;
  } else {
    return false;
  }
}
function merge(left, right) {
  var result = [];
  if (!isArray1(left)) {
    left = [left];
  }
  if (!isArray1(right)) {
    right = [right];
  }
  while (left.length > 0 && right.length > 0) {
    if (left[0] < right[0]) {
      result.push(left.shift());
    } else {
      result.push(right.shift());
    }
  }
  return result.concat(left).concat(right);
}

function mergeSort(arr) {
  var len = arr.length;
  var lim, work = [];
  var i, j, k;
  if (len == 1) {
    return arr;
  }
  for (i = 0; i < len; i++) {
    work.push(arr[i]);
  }
  work.push([]);
  for (lim = len; lim > 1;) {// lim为分组长度
    for (j = 0, k = 0; k < lim; j++ , k = k + 2) {
      work[j] = merge(work[k], work[k + 1]);
    }
    work[j] = [];
    lim = Math.floor((lim + 1) / 2);
  }
  return work[0];
}
var arr1 = [7, 5, 9, 8];
var arr2 = [7, 5, 9, 8, 3, 20, 6, 1, 2];

console.log(mergeSort(arr1)); // 5,7,9,8

console.log(mergeSort(arr2)); // 1,2,3,5,6,7,8,9,20

快速排序

以一个项为原始基点,所有分别放置左右大小,再次选择基点,再分别放置左右大小,直至排序完成。

function quick_sort(array) {
  function sort(prev, numsize) {
    var nonius = prev;
    var j = numsize - 1;
    var flag = array[prev];
    if ((numsize - prev) > 1) {
      while (nonius < j) {
        for (; nonius < j; j--) {
          if (array[j] < flag) {
            array[nonius++] = array[j]; //a[i] = a[j]; i += 1;
            break;
          };
        }
        for (; nonius < j; nonius++) {
          if (array[nonius] > flag) {
            array[j--] = array[nonius];
            break;
          }
        }
      }
      array[nonius] = flag;
      sort(0, nonius);
      sort(nonius + 1, numsize);
    }
  }
  sort(0, array.length);
  return array;
}

时间复杂度:
快速排序是一分为二的算法n/2

T(n) = 2T(n/2) + O(n)  // 有左右两次调用函数
// 根据主定理,推出:
T(n) = O(n*logn)

百科中的主定理解释

击鼓传花

游戏规则:
在这个游戏中,孩子们围成一个圆圈,把花尽快地传递给旁边的人。某一时刻传花停止,这个时候花在谁手里,谁就退出圆圈结束游戏。重复这个过程,直到只剩一个孩子(胜者)。

//** Queue
function Queue() {
  var items = [];
  this.enqueue = function (element) {
    items.push(element);
  };
  this.dequeue = function () {
    return items.shift();
  };
  this.front = function () {
    return items[0];
  };
  this.isEmpty = function () {
    return items.length == 0;
  };
  this.clear = function () {
    items = [];
  };
  this.size = function () {
    return items.length;
  };
  this.print = function () {
    console.log(items.toString());
  };
}

function hotPotato(nameList, num) {
  var queue = new Queue();  
  for (var i = 0; i < nameList.length; i++) {
    queue.enqueue(nameList[i]); 
  }
  var eliminated = '';
  while (queue.size() > 1) {
    for (var i = 0; i < num; i++) {
      queue.enqueue(queue.dequeue()); 
    }
    eliminated = queue.dequeue(); 
    console.log(eliminated + '在击鼓传花游戏中被淘汰。');
  }
  return queue.dequeue(); 
}

var names = ['A', 'B', 'C', 'D', 'E', 'F'];
var winner = hotPotato(names, 5);
console.log('胜利者:' + winner);

栈和队列

是数组和链表的一种限制

栈,只能从一头操作,(增,改都是只能操作一头),逻辑上是后进先出。
队列,可以认为是容器,也有访问的限制,逻辑上是先进先出. 新来的后面出去。

栈在实际中的应用,是函数的调用。
队列在实际中的应用,并发的产生数据,都放在队列中。


alogy
1.3k 声望121 粉丝

// Designer and Developer