阅读 276

算法复习

为面试作一些算法相关的准备~~

一、时间复杂度

通常使用最差的时间复杂度来衡量一个算法的好坏。

常数时间 O(1) 代表这个操作和数据量没关系,是一个固定时间的操作,比如说四则运算。

对于一个算法来说,可能会计算出如下操作次数 aN + 1N 代表数据量。那么该算法的时间复杂度就是 O(N)。因为我们在计算时间复杂度的时候,数据量通常是非常大的,这时候低阶项和常数项可以忽略不计。

当然可能会出现两个算法都是 O(N) 的时间复杂度,那么对比两个算法的好坏就要通过对比低阶项和常数项了。

十分钟搞定时间复杂度(算法的时间复杂度)

算法时间复杂度与空间复杂度分析

二、位运算

十进制和二进制之间的转换

位运算在算法中很有用,速度可以比四则运算快很多。

十进制33可以看成是32 + 1322^512^0。所以就是 100001.

那么二进制 100001同理,首位是 2^5 ,末位是2^0,相加得出 33

左移 <<

10 << 1
// 20
复制代码

左移就是将二进制全部往左移动,10 在二进制中表示为 1010 ,左移一位后变成 10100 ,转换为十进制也就是 20,所以基本可以把左移看成以下公式a * (2 ^ b)

左移 <<

10 >> 1
// 5
复制代码

算数右移就是将二进制全部往右移动并去除多余的右边,10 在二进制中表示为 1010 ,右移一位后变成 101 ,转换为十进制也就是 5,所以基本可以把右移看成以下公式int v = a / (2 ^ b)

  • 右移的一个用处就是在二分法的时候,计算中间值 13 >> 1 // -> 6

三、排序

两通用函数

function checkArray(arr) {
      if(!arr | arr.length <= 2) {
        return false
      }
      return true
    }

    // 交换数组中的两个值
    function swap(array, left, right) {
      let rightValue = array[right]
      array[right] = array[left]
      array[left] = rightValue
    }
复制代码

1、冒泡排序

冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 1 的位置。

    // 冒泡排序
    function sort_1 (arr) {
      // 外循环决定比较几轮次
      for(var i = arr.length-1; i > 0; i--) {
        // 内循环决定每一轮最少比较几次
        for(var j = 0; j < i; j ++) {
          if(arr[j] > arr[j+1]) {
            swap(arr, j, j+1);
          }
        }
      }
    }
    
    /**
    共有 5 元素,
    一共比4次。

    第一轮 比较 4次
    第二轮 3次
    第三轮 2次
    第四轮 1次
    最后一轮 0次
    **/
复制代码

  • 冒泡就是,每次循环将该循环中的较大元素往后面放,小的往前面放。O(n*n)

2、插入排序

插入排序原理: 将数组分为两部分,前一部分是已经排好的序列,之后是未排序的序列,我们每次从未排序的序列中拿第一个,根前面已经排好的序列中从头进行比较,找到合适的位置,进行放置。我们最初假设第一次的时候,第一个是排好的。

    // 插入排序
    function sort_2(arr) {
      // 对除第一个元素之外的元素进行向前插入
      for(var i = 1 ; i < arr.length; i++){
        // 取到未排序的序列的第一个元素,往左侧进行插入。
        for(var j = 0; j < i ; j++) {
          if(arr[j] > arr[i]) {
            swap(arr, j, i);
          }
        }
      }
    }
复制代码

3、选择排序

选择排序原理:每次从待排序的序列中找到最小的放在前面

// 每次从待排序的序列中找到最小的放在前面
    function sort_3(arr) {
      // 一共需要确定几次最小值 n-1次
      for(var i = 0 ; i < arr.length-1 ; i++) {
        // 从已经排好的之后开始,找到最小值,放入该序列的最开始,
        for(var j = i + 1; j < arr.length; j++) {
          if(arr[i] > arr[j]) {
            swap(arr, i, j);
          }
        }
      }
    }

    /**
    3 89 72  43 1

    3之后的序列中找到比3还小的,然后替换3的位置
    1 89 72 43 3

    89之后的序列中找比89还小的,然后替换89的位置
    1 3 72 43 89

    ...
    **/
复制代码

4、快速排序

基本思想是选取一个记录作为枢轴,经过一趟排序,将整段序列分为两个部分,其中一部分的值都小于枢轴,另一部分都大于枢轴。然后继续对这两部分继续进行排序,从而使整个序列达到有序。

    // 将数组划分为两部分,最终基值会在数组的中间某一位置,左侧的都比base小,右侧的都比    base大。
    function partion(arr, left, right) {

      let base = arr[left]; //基准值,默认取数组第一个元素
      // 当left和right指针相遇的时候,也就是重合的时候就跳出循环
      while(left<right) {
        // 先从右侧开始,找比base小的,然后交换位置
        while(left < right && arr[right] >= base) {
          right --;
        }
        if(left<right) {
          swap(arr, left, right);
        }
        // 再从左侧开始,找比base大的,交换位置
        while(left < right && arr[left] <= base) {
          left ++;
        }
        if( left < right) {
          swap(arr, left, right)
        }
      }
      arr[left] = base;
      return left;
    }
    // 快速排序
    function quickSort(arr, left, right) {
      let dp;
      if(left < right) {
        dp = partion(arr, left, right); // 第一步: 定位第一个基值,左侧的小,右侧的大
        quickSort(arr, left, dp-1); // 第二步: 排序左侧的
        quickSort(arr, dp+1, right); // 第三步: 排序右侧的
      }
    }
复制代码

算法题:

输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
复制代码

使用三路快排的思想:

function swap(array, left, right) {
      let rightValue = array[right]
      array[right] = array[left]
      array[left] = rightValue
    }
    var sortColors = function(nums) {
      let left = -1
      let right = nums.length
      let i = 0
      // 下标如果遇到 right,说明已经排序完成
      while (i < right) {
        // 遇到0,往左侧放
        if (nums[i] == 0) {
          swap(nums, i++, ++left)
        } else if (nums[i] == 1) {
          // 1自然就到中间了
          i++
        } else {
          // 遇到 2 往右侧放
          swap(nums, i, --right)
        }
      }
    }
复制代码
找出数组中第 K 大的元素
复制代码

使用快排来确定第K小。


function partition(items, left, right) {
    var pivot   = items[Math.floor((right + left) / 2)],
        i       = left,
        j       = right;
    while (i <= j) {
        while (items[i] < pivot) {
            i++;
        }
        while (items[j] > pivot) {
            j--;
        }
        if (i <= j) {
            swap(items, i, j);
            i++;
            j--;
        }
    }
    return i;
}

function quickSort(items, left, right, k) {
  k = k -1
  while (left < right) {
    // 分离数组后获得比基准树大的第一个元素索引
    let index = partition(items, left, right)
    // 判断该索引和 k 的大小
    if (index < k) {
      left = index + 1
    } else if (index > k) {
      right = index - 1
    } else {
      break
    }
  }

  return items[k]
}

// first call
var result = quickSort(a, 0, a.length - 1, 7);
console.log(result);
复制代码

四、链表

与数组相似,链表也是一种线性数据结构。这里有一个例子:

正如你所看到的,链表中的每个元素实际上是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。

链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。同时链表在遍历上会话费较多的时间,但是在插入和删除上却是较为方便的。

链表有两种类型:单链表双链表。上面给出的例子是一个单链表,这里有一个双链表的例子:

1、添加操作 - 单链表

与数组不同,我们不需要将所有元素移动到插入元素之后。因此,您可以在 O(1) 时间复杂度中将新结点插入到链表中,这非常高效。

2、删除操作 - 单链表

首先从头遍历链表,直到我们找到前一个结点 prev。

  • 实现一个链表:
function Node(value) {
  this.value = value;
  this.next = null;
}

function LinkList() {
  this.head = null;
  this.size = 0;
}

/**
 * [链表末尾插入]
 * @param  {[type]} val [description]
 */
LinkList.prototype.push = function(val) {
  var node = new Node(val);
  if(this.head == null) {
    this.head = node;
  } else {
    var current = this.head;
    while(current.next != null) {
      current = current.next;
    }
    current.next = node;
  }

  this.size++;
}

/**
 * 往某一个节点后插入一个节点
 * @type {Node}
 */
LinkList.prototype.insertAfter = function(value, item) {
  var node = new Node(value);
  var current = this.find(item); // 找到该节点
  if(current == null) {
    console.log('未找到该元素');
  }
  node.next = current.next;
  current.next = node;
  this.size ++;
}

/**
 * 查找某节点
 * @param  {[type]} item [元素值]
 * @return {[type]}      [description]
 */
LinkList.prototype.find = function(item) {
  var currentNode = this.head;
  if (currentNode == null) {
    console.log("这是一个空链表!!!");
    return null;
  }
  if (currentNode.value === item) {
    return currentNode;
  }
  while(currentNode&&currentNode.value != item) {
    currentNode = currentNode.next;
  }
  return currentNode;
}

/**
 * 展示
 * @return {[type]} [description]
 */
LinkList.prototype.show = function() {
  console.log('======start====')
  var current = this.head;
  var index = 0;
  while(current) {
    console.log('序号:', ++index, current.value);
    current = current.next;
  }
  console.log('======end====')
}

function remove(value) {
    var previous = this.findPrevious(value);
    var current = this.find(value);
    if (previous == null) {
      return console.log('链表中找不到被删除的元素');
    }
    previous.next = current.next;
    length--;
  }

/**
 * 删除某一个节点
 * 找到前一个节点prev。 prev.next = current.next
 * @param  {[type]} value [description]
 * @return {[type]}       [description]
 */
LinkList.prototype.remove = function(value) {
  var previous = this.findPrevious(value);
  console.log('前一个节点为', previous.value);
  var current = this.find(value);
  if (previous == null) {
    console.log('链表中找不到被删除的元素');
    return
  }
  previous.next = current.next;
  this.size--;
}

/**
 * 找到某一个节点的前一个节点
 * @param  {[type]} value [description]
 * @return {[type]}       [description]
 */
LinkList.prototype.findPrevious = function(value) {
  var current = this.head;
  if (current == null) {
    console.log('这是一个空链表');
    return null;
  }
  while(current) {
    if(current.next.value == value) {
      return current;
    }
    current = current.next;
  }
  return null;
}


var l = new LinkList();

l.push(1);
l.push(3);
l.push(4);
l.push(5);
l.push(6);

l.show();

l.remove(3);

l.show();

复制代码

单链表反转:

LinkList.prototype.reveser  = function () {
  var head = this.head;
  if ( head == undefined || !head.next == undefined ) return ;
  var p,q,r;
  p = head;
  q = p.next;
  head.next = undefined;
  while(q){
    r = q.next;
    q.next = p;
    p = q;
    q = r;
  }
  this.head = p;

};
复制代码

原理:

五、树

是一种经常用到的数据结构,用来模拟具有树状结构性质的数据集合。

树里的每一个节点有一个根植和一个包含所有子节点的列表。从图的观点来看,树也可视为一个拥有N个节点和N-1条边的一个有向无环图。

树中结点最大层次称为树的深度或高度。

如果树中结点的各个子树从左向右是有次序的,子树间不能互换位置,则称该树为有序树,否则为无序树。

森林:

1、二叉树

二叉树是一种更为典型的树树状结构。如它名字所描述的那样,二叉树是每个节点最多有两个子树的树结构,通常子树被称作 “左子树”“右子树”

满二叉树

一颗深度为k且有2^k-1个结点的二叉树称为满二叉树。

树是满的,还是二叉的

完全二叉树

从根到叶子节点,除了最后一层,其余各层都是满二叉树,只有最后一层可以不满,但是叶子结点都得靠左对齐。

比如下图,就不是一个完全二叉树,如果让第11个移动到5的右侧,那么就符合完全二叉树。

树的实现

  • (1)数组实现

这种实现方式并不好,因为如果是空结点,我们需要填充为统一的值,而且操作也不是很方便,只有取值的时候较为方便。

  • (2)链表形式

每个结点记录其左孩子和右孩子。

针对实际情况,如果我们有一堆数据,如果其本身符合树类型,我们可以直接使用链表形来存储,否则我们可以借助于数组(不建议),以及通过构建二叉搜索树来存储我们的数据(推荐)。

2、二叉搜索树

【二叉树搜索树】 (BST) 是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大(或者等于)的值。

这种存储方式很适合于数据搜索。如下图所示,当需要查找 6 的时候,因为需要查找的值比根节点的值大,所以只需要在根节点的右子树上寻找,大大提高了搜索效率。

二叉搜索树的插入操作

  • 插入一个6:

首先会检测二叉树是否为空? 
第二检测根节点(key[6] < root[11]为真),然后继续检测(node.left不是null),到达node.left[7]节点。 
第三检测(key[6] < key[7]为真),然后继续检测(node.left不是null),到达node.left[5]节点。 
最后检测(key[6] < key[5]为真),然后继续检测(node.right不是null),为空添加在key[5]右节点添加key[6]。 
复制代码
  • 移除一个节点5:

首先会检测二叉树是否为空? 
第二检测根节点(key[5] = root[11]为真),然后检查(key[5] < root[11])然后继续检测(node.left不是null),到达node.left[7]节点。 
第三检测根节点(key[5] = key[7]为真),然后检查(key[5] < key[7]为真),然后继续检测(node.left不是null),到达node.left[5]节点。 
第四检测(key[5] = key[5]为真),然后删除 key[5]节点。 
最后(key[5] )子节点,key[3]的父节点改成原来key[5]的父节点key[7]。
复制代码

3、树的遍历

前序遍历

中序遍历

对于二叉搜索树,中序遍历可以对其进行从小到达排序。

后序遍历

值得注意的是,当你删除树中的节点时,删除过程将按照后序遍历的顺序进行。 也就是说,当你删除一个节点时,你将首先删除它的左节点和它的右边的节点,然后再删除节点本身。

另外,后序在数学表达中被广泛使用。

4、二叉搜索树的实现


/**
 * 节点类
 * @param       {[type]} val [description]
 * @constructor
 */
function Node(val) {
  this.key = val;
  this.left = this.right = null;
}

/**
 * 二叉搜索树 BST
 * @constructor
 */
function BinaryTree() {
  this.root = null;
  this.size = 0;
}

BinaryTree.prototype.constructor = BinaryTree;

/**
 * 向二叉树插入一个新的值
 * @param  {[type]} key [值]
 * @return {[type]}     [description]
 */
BinaryTree.prototype.insert = function(key) {
  var newNode = new Node(key);
  if(this.root === null) {
      this.root = newNode;
  } else {
      this.insertNode(this.root, newNode);
  }
}

/**
 * 往某一个节点下插入一个新的节点
 * @param  {[type]} node    [description]
 * @param  {[type]} newNode [description]
 * @return {[type]}         [description]
 */
BinaryTree.prototype.insertNode = function(node,newNode) {
  if(node.key > newNode.key){
      if(node.left==null){
        node.left=newNode;
      }else{
        this.insertNode(node.left,newNode)
      }
    }else{
      if(node.right==null){
        node.right=newNode
      } else {
        this.insertNode(node.right,newNode)
      }
    }
}

/**
 * 获取根节点
 * @return {[type]} [description]
 */
BinaryTree.prototype.getRoot = function() {
  return this.root;
}

/**
 * 中序遍历,从根开始
 * @return {[type]} [description]
 */
BinaryTree.prototype.inOrderTraverse = function(callback) {
  this.inOrderTraverseNode(this.root, callback);
}


/**
 * 中序遍历从某一节点开始的子节点
 * @param  {[type]}   node     [description]
 * @param  {Function} callback [description]
 * @return {[type]}            [description]
 */
BinaryTree.prototype.inOrderTraverseNode = function(node, callback) {
  if(node!=null) {
    this.inOrderTraverseNode(node.left, callback); // 遍历左侧
    callback(node.key); // 输出节点
    this.inOrderTraverseNode(node.right, callback); // 遍历右侧
  }
}


/**
 * 先序遍历,从根开始
 * @return {[type]} [description]
 */
BinaryTree.prototype.preOrderTraverse = function(callback) {
  this.preOrderTraverseNode(this.root, callback);
}


/**
 * 中序遍历从某一节点开始的子节点
 * @param  {[type]}   node     [description]
 * @param  {Function} callback [description]
 * @return {[type]}            [description]
 */
BinaryTree.prototype.preOrderTraverseNode = function(node, callback) {
  if(node!=null) {
    callback(node.key); // 输出节点
    this.preOrderTraverseNode(node.left, callback); // 遍历左侧
    this.preOrderTraverseNode(node.right, callback); // 遍历右侧
  }
}


/**
 * 后序遍历,从根开始
 * @return {[type]} [description]
 */
BinaryTree.prototype.postOrderTraverse = function(callback) {
  this.postOrderTraverseNode(this.root, callback);
}


/**
 * 后序遍历从某一节点开始的子节点
 * @param  {[type]}   node     [description]
 * @param  {Function} callback [description]
 * @return {[type]}            [description]
 */
BinaryTree.prototype.postOrderTraverseNode = function(node, callback) {
  if(node!=null) {
    this.postOrderTraverseNode(node.left, callback); // 遍历左侧
    this.postOrderTraverseNode(node.right, callback); // 遍历右侧
    callback(node.key); // 输出节点
  }
}


// =========测试

var tree = new BinaryTree();

tree.insert(11);
tree.insert(7);
tree.insert(15);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
tree.insert(18);
tree.insert(25);
tree.insert(6);

// //11,7,15,5,3,9,8,10,13,12,14,20,18,25,6
function printNode(value){
    console.log(value);
}

// 中序遍历
// tree.inOrderTraverse(printNode);

// 先序遍历
// tree.preOrderTraverse(printNode);

// 后序遍历
tree.postOrderTraverse(printNode);

复制代码

以上实现了一个二叉搜索树,并测试了三种遍历手段。

以上的这几种遍历都可以称之为深度遍历,对应的还有种遍历叫做广度遍历,也就是一层层地遍历树。对于广度遍历来说,我们需要利用之前讲过的队列结构来完成。

以下是广度遍历:

原理:父节点出队列,他的左右子节点进队列

breadthTraversal() {
  if (!this.root) return null
  let q = new Queue()
  // 将根节点入队
  q.enQueue(this.root)
  // 循环判断队列是否为空,为空
  // 代表树遍历完毕
  while (!q.isEmpty()) {
    // 将队首出队,判断是否有左右子树
    // 有的话,就先左后右入队
    let n = q.deQueue()
    console.log(n.value)
    if (n.left) q.enQueue(n.left)
    if (n.right) q.enQueue(n.right)
  }
}
复制代码

如何在树中寻找最小值或最大数。因为二分搜索树的特性,所以最小值一定在根节点的最左边,最大值在最右边。代码如下:

/**
 * 获取整个树的最小值
 * @return {[type]}            [description]
 */
BinaryTree.prototype.getMin = function() {
  return this.getMinNode(this.root);
}

/**
 * 获取某一个节点之下的最小值
 * @param  {[type]} node [description]
 * @return {[type]}      [description]
 */
BinaryTree.prototype.getMinNode = function(node) {
  if(node) {
    while(node && node.left !== null) {
        node = node.left;
    }
    return node.key;
  }
  return null;
}

/**
 * 获取整个树的最大值
 * @return {[type]}            [description]
 */
BinaryTree.prototype.getMax = function() {
  return this.getMaxNode(this.root);
}

/**
 * 获取某一个节点之下的最大值
 * @param  {[type]} node [description]
 * @return {[type]}      [description]
 */
BinaryTree.prototype.getMaxNode = function(node) {
  if(node) {
    while(node && node.right !== null) {
        node = node.right;
    }
    return node.key;
  }
  return null;
}
复制代码
  • 如果是获取第k小元素

思路: 使用中序遍历,得到的就是一个排好序的数组,然后取其第k-1项。

  • 查找某一个值是否存在,存在返回1,不存在返回-1
/**
 * 查找某一个值是否存在
 * @param  {[type]}   k     [description]
 * @return {[type]}            [description]
 */
BinaryTree.prototype.findOne = function(k) {
  var node = this.root;
  while(node!=null) {
    // 小于结点值,去左侧找
    if(node.key > k) {
      // 注意这里,使用子结点的时候不妨判断一下
      if(node.left) {
        node = node.left;
      } else {
        return -1;
      }
      
    }
    // 大于结点值,去右侧找
    if(node.key < k) {
      if(node.right) {
        node = node.right;
      } else {
        return -1;
      }
    }
    // 等于结点的话就表示找到了
    if(node.key == k) {
      return 1;
    }
  }
  return -1;
}

复制代码
  • 二叉搜索树的删除节点操作:

会存在以下几种情况

需要删除的节点没有子树
需要删除的节点只有一条子树
需要删除的节点有左右两条树
复制代码

代码如下:

/**
 * 移除某一个元素
 * @param  {[type]} element [description]
 * @return {[type]}         [description]
 */
BinaryTree.prototype.remove = function(element) {
  return this.removeNode(this.root, element);
}

BinaryTree.prototype.removeNode = function(node, element) {
    if(node === null) {
        return null;
    }

    // 首先确定要删除的节点的位置
    if(element < node.key) {
        // 如果是比当前节点小,就去左侧找
        // 返回一个新的左子树
        node.left = this.removeNode(node.left, element);
        return node;
    } else if(element > node.key) {
        // 如果是比当前节点大,就去右侧找
        // 返回一个新的右子树
        node.right = this.removeNode(node.right, element);
        return node;
    } else {
        // 找到该节点之后

        // 叶子节点,直接置为null
        if(node.left === null && node.right === null) {
            node = null;
            return node;
        }

        // 左子树为空
        if(node.left === null) {
            node = node.right;
            return node;

        } else if(node.right === null) {
            // 右子树为空
            node = node.left;
            return node;
        }


        // 左右都不为空
        var aux = this.findMinNode(node.right); // 找出右子树的最小
        node.key = aux.key;
        node.right = this.removeNode(node.right, aux.key);
        return node;
    }

}

function findMinNode(node) {
    if (node) {
        while (node && node.left !== null) {
            node = node.left
        }
        return node
    }
    return null
}
复制代码

4、AVL树(平衡二叉树)

AVL树本质上是一颗二叉查找树,但是它又具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为平衡二叉树。下面是平衡二叉树和非平衡二叉树对比的例图:

AVL树的作用

二分搜索树实际在业务中是受到限制的,因为并不是严格的 O(logN),在极端情况下会退化成链表,比如加入一组升序的数字就会造成这种情况。由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏。

例如:我们按顺序将一组数据1,2,3,4,5,6分别插入到一颗空二叉查找树和AVL树中,插入的结果如下图:

看了上诉痛点,比如我们在拿到一个【有序】序列后,我们如何来建立一个较好的平衡二叉搜索树呢?

例如上图:

我们可以先拿到中间的一个树26作为跟结点,然后26左侧取最中间的一个数作为左子树,26右侧取中间的一个数做为右子树,依次类推,一个平衡二叉树就ok了。但是前提条件是有序序列

AVL树的基本操作

AVL树的操作基本和二叉查找树一样,这里我们关注的是两个变化很大的操作:插入和删除! 我们要做一些特殊的处理,包括:单旋转和双旋转:

强烈推荐——【漫画】以后在有面试官问你AVL树,你就把这篇文章扔给他。

1、左-左型:做右旋。

2、右-右型:做左旋转。

3、左-右型:先做左旋,后做右旋。

4、右-左型:先做右旋,再做左旋。
复制代码

六、面试题

值得推荐—— 一文高效图解二叉树面试题

理解 TreeNode 数据结构

如何确定二叉树的最大深度或者最小深度

/**
 * 求二叉树的最大深度
 * @param  {[type]}   根结点     [description]
 * @return {[type]}            [description]
 */
BinaryTree.prototype.getMaxDepth = function(node) {
  if(node == null) {
    // 此处是关键点
    return 0;
  }

  int leftmax = getMaxDepth(node.left); // 左侧最大
  int rightmax = getMaxDepth(node.right); // 右侧最大

  return Math.max(leftmax, rightmax) + 1; // 取最大
}

思路:
1、如果是根结点为空,直接返回0
2、求左子树的最大深度
3、求右子树的最大深度
4、取两者最大值 + 1
复制代码

求最小深度:

BinaryTree.prototype.getMinDepth = function(root) {
  //  根结点为空
  if(node == null) {
    return 0;
  }
  return getMin(root);
}

BinaryTree.prototype.getMin = function(node) {
  // 如果节点的左右儿子都不存在,说明只有自己,节点深度为1.
  if(node.left == null && node.right == null) {
    return 1;
  }
  if(node.left == null) {
    return getMin(node.right) + 1; // 递归之后要加一,加上node结点所在的那一层
  } else if(node.right == null) {
    return getMin(node.left) + 1;
  }
  return Math.min(getMin(node.left), getMin(node.right)) + 1;
}

思路:
1、如果是根结点为空,直接返回0
2、如果是叶子结点,直接返回1
3、如果没有左子树为空,则递归右子树,求的的深度+1,就是该节点的深度
4、如果没有右子树为空,则递归左子树,求的的深度+1,就是该节点的深度
5、递归取左右子树深度的最小值 + 1
复制代码

如何确定二叉树是否是平衡二叉树

BinaryTree.prototype.isBalanced = function(root) {
  return maxDeath2(root) != -1;
}

// 左右子树深度之差大于1,那么就表示不是平衡二叉树
BinaryTree.prototype.maxDeath2 = function(root) {
  if(root == null) {
    return 0;
  }

  var left = maxDeath2(node.left);
  var right = maxDeath2(node.right);

  // left = -1 : 遍历左子树的时候,出现了不平衡现象
  // right = -1:  遍历右子树的时候,出现了不平衡现象
  // Math.abs(a-b) > 1: 左右子树深度之差大于1,那么就表示不是平衡二叉树
  if(left == -1 || right == -1 || Math.abs(a-b) > 1) {
    return -1; // 如果不是返回-1
  }
  return Math.max(left, right) + 1;
}
复制代码

搜索二叉树如何实现插入

值为 2 的节点开始判断
如果为空,则插入该节点
循环下面节点:
节点当前值大于,继续循环左节点
节点当前值小于,继续循环右节点
复制代码

搜索二叉树如何实现查找

搜索二叉树如何实现删除

比较复杂了。相比新增、搜搜,删除需要将树重置。逻辑为:删除的节点后,其替代的节点为,其右节点树中值最小对应的节点。如图:

思路:
需要删除的节点没有子树,直接删除即可
需要删除的节点只有一条子树:删除的节点后,那么让其子树的结点代替原来的位置即可
需要删除的节点有左右两条树,删除的节点后,其替代的节点为,其右节点树中值最小对应的节点。
复制代码

七、数组

js中的数组我们应该都很熟悉了,但是还是有一些小的知识点需要我们了解的:

1、js中的数组是通过Map的形式存储的,它的索引实际上就是Map的键,我们可以证明,(arr['h'] = 10这么写是不会报错的,因此是Map结构)
2、我们之前认识的数组,是连续存储的,也就是地址的连续的,声明数组的时候我们需要指定它的长度,长度受限制。
3、js的数组则较为灵活,既可以指定默认的长度,同时还可以随时扩充数组的长度,中间的空缺值会默认赋值为undefined
4、自带length属性,可以随时获取数组的长度
5、平时我们只需将其看作是一种顺序存储即可
复制代码

数组是JS面试中必考考点,面试官可能出一些算法题考验你的基础掌握水平~

1、数组常用操作

合并数组

arr1.concat(arr2,arr3...)

var hege = ["Cecilie", "Lone"];
var stale = ["Emil", "Tobias", "Linus"];
var kai = ["Robin"];
var children = hege.concat(stale,kai);
复制代码

检查数组中是否存在某个元素,并返回索引位置

arr.indexOf('a')

var fruits = ["Banana", "Orange", "Apple", "Mango"];
var a = fruits.indexOf("Apple");
复制代码

如果没找到返回-1

数组插入操作

arr.push(); // 末尾插入,并返回新的长度。
arr.unshift(); // 开头插入,并返回新的长度
arr.splice(); // 中间插入,这个函数可以用于插入、删除、替换

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.push("Kiwi")

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.unshift("Lemon","Pineapple");

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.splice(2,0,"Lemon","Kiwi");
// "Banana", "Orange", "Lemon","Kiwi", "Apple", "Mango"
复制代码

数组元素的删除

arr.pop();  // 末尾删除,并返回删除的元素
arr.shift(); // 首部删除
arr.splice(); // 中间删除

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.pop();

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.shift();

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.splice(2,1,"Lemon","Kiwi");
// "Banana", "Orange", "Lemon","Kiwi", "Mango"

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.splice(2,1);
// "Banana", "Orange", "Mango"
复制代码
array.splice(index,howmany,item1,.....,itemX)
第一个参数表示添加或者删除的位置索引
第二个参数表示要删除多少个元素,如果是插入为0
第三个参数以后为要插入的元素
复制代码

Array内置对象有自己的方法,因此我们不需要手动去编写这些函数了。如果是在首尾插入和删除那么负责度可能是O(1);如果是中间插入和删除平均复杂度为O(n/2);因为我们需要移动其他的元素来实现插入和删除。

数组的遍历

数组的遍历我们可以使用for循环、forEach循环。map方法可以遍历我们的数组元素,执行某个自定义方法来返回新的数组。

var arr = [1,2,3,4];

arr.map((item) => {
    return item * 2;
})

// [2,4,6,8]
复制代码

数组的筛选

arr.filter(); // 检测数组元素,并返回符合条件所有元素的数组。
arr.every(); // 检测数组元素的每个元素是否都符合条件,如果符合会true反之false
arr.some(); // 检测数组元素的是否存在一个元素符合条件,如果符合会true反之false

var arr = [1,2,3,4];

arr.filter((item) => {
    return item > = 2;
})
// [2,3,4]

// every
var ages = [32, 33, 16, 40];

function checkAdult(age) {
    return age >= 18;
}

function myFunction() {
    document.getElementById("demo").innerHTML = ages.every(checkAdult);
}
// some
var ages = [3, 10, 18, 20];

function checkAdult(age) {
    return age >= 18;
}

function myFunction() {
    document.getElementById("demo").innerHTML = ages.some(checkAdult);
}

复制代码

数组的排序

arr.sort() // 默认是字母升序排列
arr.reverse(); // 反转数组
// 生序
var points = [40,100,1,5,25,10];
points.sort(function(a,b){return a-b});

// 降序
var points = [40,100,1,5,25,10];
points.sort(function(a,b){return b-a});
复制代码

数组的剪切

slice(start, end); // [start, end) 左闭右开

var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
var citrus = fruits.slice(1,3);
// Orange,Lemon
复制代码

数组的其他操作

数组拼接
var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
var citrus = fruits.join("-");
// Banana-Orange-Lemon-Apple-Mango
复制代码

2、数组冒泡排序

function sort(arr) {
    var flag;
    // 外层循环执行 n - 1 次 
    for(var i = 0; i < arr.length -1; i ++) {
        flag = false;
      // 内层循环执行次数根据 轮次来定, -1 表示处理到前一个
        for(var j = 0; j < arr.length - 1 - i; j++) {
            if(arr[j] > arr[j+1]) {
                swap(arr, j, j+1);
                flag = true;
            }
            
        }
        // 如果该轮次没有进行交换,表示已经是有序了,直接跳过,增加性能 
        if(!flag) {
              break;
        }
    }
  console.log(arr);
}
function swap(arr, i, j) {
  var temp = arr[j];
  arr[j] = arr[i];
  arr[i] = temp;
}

sort([1,3,5,2,7,3,2,112,34,23,111]);
复制代码

冒泡排序是众多排序的基础,最坏时间复杂度为O(n^2);经过优化后可以降低不少~~

3、数组常考面试问题

1.翻转字符串

function reverseStr (s)  {
  var arr = s.split("");
  return arr.reverse().join("");
}

console.log(reverseStr('asdf'));
复制代码

2、找到句中最长的单词

function reverseStr (s)  {
  var arr = s.split(" ");
  var result = arr[0];
  for(var i = 1 ; i < arr.length; i++) {
    if(arr[i].length > result.length) {
      result = arr[i];
    }
  }
  return result;
}

console.log(reverseStr('asdf is sadsadasdasd dsa'));
复制代码

3、字符串回文判断

如:字符串abccba,从前往后读是a-b-c-c-b-a;从后往前读也是a-b-c-c-b-a

function reverseStr (s)  {
  var arr = s.split("");
  var str = '';
  for(var i = arr.length-1 ; i >= 0; i--) {
    if(arr[i]) {
      str+= arr[i];
    }
  }
  console.log(str);
  console.log(s);
  return s == str;
}

console.log(reverseStr('abccba'));
复制代码

4、首字母大写

function reverseStr (s)  {
  return s.toLowerCase().split(" ").map((item) => {
      return item.split("").map((v, i) => {
        if(i === 0) {
          return v.toUpperCase();;
        } else {
          return v;
        }
      }).join("");
  }).join(" ");
}

console.log(reverseStr('abc cba is arr'));

// "Abc Cba Is Arr"
复制代码

5、拔尖:在已知包含了4个小数组的大数组中,分别找到每个小数组中的最大值,然后把它们串联起来,形成一个新数组。

function largestOfFour (s)  {
  return s.map((item) => {
    var arr = [];
    item.sort((a, b) => {
      return a - b;
    })
    arr.push(item[item.length-1]);
    return arr;
  })
}
console.log(largestOfFour([
    [4, 5, 1, 3], 
    [13, 27, 18, 26], 
    [32, 35, 37, 39], 
    [1000, 1001, 857, 1]
]));

// [[5], [27], [39], [1001]]
复制代码

6、切割数组

chunk([1,2,3,4],2)=[[1,2],[3,4]]; chunk([1,2,3,4,5],2)=[[1,2],[3,4],[5]];

function chunk(arr,num){
    var newArr = [];
    for(i = 0; i < arr.length;i = i + num){
        newArr[newArr.length] = arr.slice(i,i + num);
    }
    return newArr;
}
console.log(chunk([1,2,3,4,5],2)); //[[1,2],[3,4],5]
复制代码

7、找儿子 如果数组第一个字符串元素包含了第二个字符串元素的所有字符,函数返回true。例:["hello", "Hello"]应该返回true,因为在忽略大小写的情况下,第二个字符串的所有字符都可以在第一个字符串找到.["hello", "hey"]应该返回false,因为字符串"hello"并不包含字符"y"。 ["Alien", "line"]应该返回true,因为"line"中所有字符都可以在"Alien"找到。

思路是使用indexOf来查找:

function mutation(arr) {
    var index = true;
    for(i = 0; i < arr[1].length; i++){
        if(arr[0].toLowerCase().indexOf(arr[1].toLowerCase().split("")[i]) < 0){
            index = false;
        }
    }
    return index;
}

document.write(mutation(["IloveJoe","joe"]));//ture;
}
复制代码

8、判断是否是以某一个字符串结尾

function largestOfFour (str, target)  {
  var s1 = str.split("")
            .splice( str.length - target.length, target.length)
            .join("");
  return s1 === target;
}
console.log(largestOfFour('adsadbarrr', 'arrr')); // true
复制代码

9、将稀疏数组调整为不稀疏数组

function noSparse(arr) {
    var resArr = [];   //创建空数组
    for (var i = 0; i < arr.length; i ++) {
        if (arr[i] !== undefined) {
            resArr.push(arr[i])
        }
    }
    return resArr;
}

var arr_demo1 = [1,2,,,,,3,4,,'a',['A','B','C'],,'M'];
console.log(noSparse( arr_demo1 ) );
// [1, 2, 3, 4, "a", ["A", "B", "C"], "M"]
复制代码

10、数组去重复

  • 二重循环去重
function toRepeat(arr) {
  var isRepeat = false;
  var tempArr = [];
  for(var i = 0 ; i < arr.length; i ++) {
    isRepeat = false;
    for(var j = 0; j < tempArr.length; j++) {
      if(tempArr[j] == arr[i]) {
        isRepeat = true;
      }
    }
    if(!isRepeat) {
      tempArr.push(arr[i]);
    }
    
  }
  return tempArr;
}
console.log(toRepeat([1,2,3,4,2,3,6,7]));
复制代码
  • indexOf循环去重
function toRepeat(arr) {
  var tempArr = [];
  arr.sort((a,b) => a-b); // 先排序
  
  for(var i = 0 ; i < arr.length; i ++) {
    if(tempArr.indexOf(arr[i]) == -1) {
      tempArr.push(arr[i]);
    } 
  }
  return tempArr;
}
console.log(toRepeat([1,2,3,4,2,3,6,7]));
复制代码
  • sort后去重
function toRepeat(arr) {
  var tempArr = [];
  arr.sort((a,b) => a-b); // 先排序
  
  for(var i = 0 ; i < arr.length; i ++) {
    if(tempArr[tempArr.length-1] != arr[i]) {
        tempArr.push(arr[i]);
    }
  }
  return tempArr;
}
console.log(toRepeat([1,2,3,4,2,3,6,7]));
复制代码
  • Map
function toRepeat(arr) {
  var tempArr = [];
  var map = new Map();
  
  
  for(var i = 0 ; i < arr.length; i ++) {
    if(!map.get(arr[i])) {
      map.set(arr[i], 1);
      tempArr.push(arr[i]);
    }
  }
  return tempArr;
}
console.log(toRepeat([1,2,3,4,2,3,6,7]));
复制代码
  • Set
function toRepeat(arr) {
  const set = new Set(arr);
  return Array.from(set);
}
console.log(toRepeat([1,2,3,4,2,3,6,7]));

function toRepeat(arr) {
  return [...new Set(arr)];
}
console.log(toRepeat([1,2,3,4,2,3,6,7]));
复制代码

综合分析上诉方法,Map和Set方式最快,sort方法居中,二重循环和indexOf最慢且差距明显。建议使用Map或者Set方式

11、打平嵌套数组 [1, [2, [3], 4], 5] => [1, 2, 3, 4, 5]

使用递归

const arr = [1,
             [2,
              [3, 
               [5,9,
                [100,233]
               ]
              ],
              4],
             5];
var arr1 = [1,[2,3],4];

function flatten(arr) {
    for (let i in arr) {
        if (Array.isArray(arr[i])) {
            arr.splice(i, 1, ...flatten(arr[i]))
        }
    }
    return arr
}
console.log(flatten(arr)) // [1, 2, 3, 5, 9, 100, 233, 4, 5]
console.log(flatten(arr1))
// 
复制代码

来一个更加简洁,但是使用需谨慎的~~

var arr1 = [1,[2,3,[1,4,5,[6,7,[4,5]],3,4]],4];

function flatten1(arr) {
    return arr.join(',').split(',') // 其实相当于使用了toString方法来提取所有的数值
}
// ["1", "2", "3", "1", "4", "5", "6", "7", "4", "5", "3", "4", "4"]

// 与上面不同的是,最后的数组元素是字符串
复制代码

12、找到两个有序数组中的最小公共数

const a = [1, 2, 5, 9, 10]
const b = [3, 4, 6, 9, 10]

function findCommon(a, b) {
  var i = j = 0;
  while(i < a.length && j < b.length) {
    if(a[i] == b[j]) {
      return a[i];
    } else if(a[i] > b[j]){
       j++;
    } else if (a[i] < b[j]) {
       i++;
    }
  }
  return -1;
}

console.log(findCommon(a,b));
// 9
复制代码

13、判断是一个数组

Object.prototype.toString().call(arr) == '[object Array]'
Array.isArray();
arr.constructor == Array
复制代码

15、快速提取数组中的最大值

Math.max.apply({}, [1,3,5,6,7]);

function max (arr) {
    return Math.max.apply({}, arr);
}
复制代码
Array.prototype.max = function() {
    var max = this[0]
    this.forEach(function(v) {
        if (v > max) {max = v}
    })
    return max
}
[1,45,23,3,6,2,7,234,56].max() // 234
复制代码
var arr1 = [1,4,5,2];
function max (arr) {
    return arr.reduce((prev, next) => {
      return prev > next ? prev : next;
    })
}
console.log(max(arr1))
复制代码
关注下面的标签,发现更多相似文章
评论