常见的算法题

常见的算法

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。

function selectSort (arr) {
    // 如果数组长度小于2,则返回原数组
    if (arr && arr.length < 2) {
        return arr;
    }

    let index, temp;
    const len = arr.length;
    for (let i = 0; i < len - 1; i++) { // 这里使用len-1是因为最后一次最后一个值肯定是最大的了,所以没必要再执行一次循环操作
        index = i;
        for (let j = i + 1; j < len; j++) {
            if (arr[j] < arr[index]) { // 找出最小值
                index = j; // 保存最小值的索引
            }
        }
        // 交换两个值
        temp = arr[i];
        arr[i] = arr[index];
        arr[index] = temp;
    }
    return arr;
}

var arr = [3, 1, 5, 9, 4, 8, 7, 10, 2, 6];
selectSort(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

冒泡排序

冒泡排序(Bubble Sort)一种稳定排序算法,冒泡排序算法的原理如下:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
function BubbleSort (arr) {
    // 如果数组长度小于2,则返回原数组
    if (arr && arr.length < 2) {
        return arr;
    }

    let temp;
    const len = arr.length;

    for (let i = 0; i < len - 1; i++) { // 外层循环只需比较length-1次
        for (let j = 0; j < len - 1 - i; j++) { // 内层循环,只需遍历比较未排序的
            if (arr[j] > arr[j + 1]) { // 两两比较
                // 交换值,大的值放后面
                // [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]]; // 可以用es6数组语法交换值
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}

var arr = [3, 1, 5, 9, 4, 8, 7, 10, 2, 6];
BubbleSort(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。
原理:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

function quickSort(arr) {
    // 如果数组长度小于2,则返回原数组
    if (arr && arr.length < 2) {
        return arr;
    }

    let leftArr = []; // 存储小于分界值的数组
    let rightArr = []; // 存储大于等于分界值的数组
    const prev = arr.splice(0, 1)[0]; // 取出数组的第一个值作为分界值(这个分界值可以是数组中的任意一个值)
    const len = arr.length; // 数组长度必须在取出分界值之后获取
    for (let i = 0; i < len; i++) {
        arr[i] < prev ? leftArr.push(arr[i]) : rightArr.push(arr[i]);
    }
    return quickSort(leftArr).concat(prev, quickSort(rightArr));
}

var arr = [3, 1, 5, 9, 4, 8, 7, 10, 2, 6];
quickSort(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

使用ES6简洁实现

function quickSort (arr) {
    // 如果数组长度小于2,则返回原数组
    if (arr && arr.length < 2) {
        return arr;
    }

    const [prev, ...rest] = arr;
    return [
        ...quickSort(rest.filter(value => value < prev)), // 筛选出比prev的值小的值放在prev的左边,然后再进行递归操作
        prev,
        ...quickSort(rest.filter(value => value >= prev)), // 筛选出比prev的值大的放在prev的右边,然后再进行递归操作
    ]
}

var arr = [3, 1, 5, 9, 4, 8, 7, 10, 2, 6];
quickSort(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

插入排序

function insertSort (arr) {
    let res = [];
    res.push(arr[0]);
    for (let i = 1; i < arr.length; i++) {
        let cur = arr[i];
        for (let j = res.length; j >= 0; j++) {
            if (cur > res[j]) {
                res.splice(j + 1, 0, cur);
                break;
            }
            if (j === 0) {
                res.unshift(res[j]);
            }
        }
    }
    return res;
}
var arr = [3, 1, 5, 9, 4, 8, 7, 10, 2, 6];
insertSort(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

二叉树的深度优先遍历、广度优先遍历、层序遍历

const root = {
    value: 1,
    left: {
        value: 2,
        left: {
            value: 4
        },
        right: {
            value: 5
        },
    },
    right: {
        value: 3,
        left: {
            value: 6
        },
        right: {
            value: 7
        },
    },
}

// 深度优先遍历
function dfs(root) {
    const helper = (root) => {
        if (!root) return null
        console.log(root.value)
        return helper(root.left) || helper(root.right)
    }
    return helper(root)
}

// 测试结果:
dfs(root)

console.log('\n')

// 广度优先遍历
function bfs(root) {
    const queue = [root]
    while (queue.length) {
        const node = queue.shift()
        console.log(node.value)
        if (node.left) queue.push(node.left) 
        if (node.right) queue.push(node.right) 
    }
}

// 测试结果:
bfs(root)

console.log('\n')

// 层序遍历,每层生成数组
function cfs(root) {
    const ans = []
    const queue = [root]
    while (queue.length) {
        const cur = queue.length
        const level = []
        for (let i = 0; i < cur; i++) {
            const node = queue.shift()
            level.push(node.value)
            if (node.left) queue.push(node.left)
            if (node.right) queue.push(node.right)
        }
        ans.push(level)
    }
    return ans
}

// 测试结果:
cfs(root)

多叉树的层序遍历

// 多叉树的结构
var tree = {
    val: 1,
    children: [{
        val: 2,
        children: [{
            val: 5,
            children: [{
                val: 12,
            }, ],
        }, {
            val: 6,
            children: [{
                val: 13,
            }, {
                val: 14,
            }, {
                val: 15,
            }, {
                val: 16,
            }, ],
        }, {
            val: 7,
        }, ],
    }, {
        val: 3,
        children: [{
            val: 8,
            children: [{
                val: 17,
            }, ],
        }, ],
    }, {
        val: 4,
    }, ],
};

// 实现代码
var levelOrder = function(root) {
    if (!root) {
        return [];
    }

    const ans = [];
    const queue = [root];

    while (queue.length) {
        const cnt = queue.length;
        const level = [];
        for (let i = 0; i < cnt; ++i) {
            const cur = queue.shift();
            level.push(cur.val);
            if (cur.children) {
                for (const child of cur.children) {
                    queue.push(child);
                }
            }
        }
        ans.push(level);
    }

    return ans;
};

// 测试结果:
console.log(levelOrder(tree))

反转字符串

var reverseWords = function (str) {
        if (!str) return;
        let left = 0;
        let right = str.length - 1;
        let queue = [];
        let word = "";
        // 去除字符串两端的空格
        while (str.charAt(left) === " ") left++;
        while (str.charAt(right) === " ") right--;
        // 逐个读取字符串中的每个单词,依次放入双端队列的队头
        while (left <= right) {
          let char = str.charAt(left);
          if (char === " " && word) {
            queue.unshift(word);
            word = "";
          } else if (char !== " ") {
            word += char;
          }
          left++;
        }
        queue.unshift(word);
        return queue.join(" ");
      };

      // 测试结果:
      reverseWords("  hello world!  "); // "world! hello"
      reverseWords("a good   example"); // "example good a"

求字符串中,出现次数最多的字符

var str = "abcabcabcbbccccc";

function findMaxChar(str) {
  var num = 0;
  var char = "";

  // 使其按照一定的次序排列
  str = str.split("").sort().join("");
  // "aaabbbbbcccccccc"

  // 定义正则表达式
  var re = /(\w)\1+/g;
  str.replace(re, ($0, $1) => {
    if (num < $0.length) {
      num = $0.length;
      char = $1;
    }
  });
  console.log(`字符最多的是${char},出现了${num}次`); // 字符最多的是c,出现了8次
  return {
    char,
    num,
  };
}

给定一个只包括 ‘(‘ ,’)’ ,’{‘ ,’}’ ,’[’ ,’]’ 的字符串,判断字符串是否有效。

var isValidStr = function (str) {
  var map = {
    "{": "}",
    "[": "]",
    "(": ")",
  };
  var stack = [];
  for (let i = 0; i < str.length; i++) {
    if (map[str[i]]) {
      stack.push(str[i]);
    } else if (str[i] !== map[stack.pop()]) {
      return false;
    }
  }
  // 当遍历完成时,所有已匹配的字符都已匹配出栈,如果此时栈为空,则字符串有效,如果栈不为空,说明字符串中还有未匹配的字符,字符串无效
  return stack.length === 0;
};

// 测试结果:
isValidStr("{[()]}"); // true
isValidStr("{}()"); // true
isValidStr("{[)]}"); // false
isValidStr("([]}"); // false

删除字符串中的所有相邻重复项

示例

输入:”abbaca” 输出:”ca” 解释: 例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。

解法:利用栈 解题思路: 遍历字符串,依次入栈,入栈时判断与栈头元素是否一致,如果一致,即这两个元素相同相邻,则需要将栈头元素出栈,并且当前元素也无需入栈

解题步骤: 遍历字符串,取出栈头字符,判断当前字符与栈头字符是否一致

不一致,栈头字符进栈,当前字符进栈 一致,即栈头字符与当前字符相同相邻,都不需要进栈,直接进入下次遍历即可 遍历完成后,返回栈中字符串

function deleteRepeatChar(str) {
  var stack = [];
  for (let s of str) {
    var prev = stack.pop();
    if (s !== prev) {
      stack.push(prev);
      stack.push(s);
    }
  }
  return stack.join("");
}

// 测试结果:
var str = "abbaca";
deleteRepeatChar(str); // ca
str = "abccdddaacb";
deleteRepeatChar(str); // abdcb

给定一个字符串,逐个翻转字符串中的每个单词。

示例

输入: "the sky is blue"
输出: "blue is sky the"

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

说明:

解题思路:使用双端队列解题

var reverseWords = function (str) {
  if (!str) return;
  let left = 0;
  let right = str.length - 1;
  let queue = [];
  let word = "";
  // 去除字符串两端的空格
  while (str.charAt(left) === " ") left++;
  while (str.charAt(right) === " ") right--;
  // 逐个读取字符串中的每个单词,依次放入双端队列的队头
  while (left <= right) {
    let char = str.charAt(left);
    if (char === " " && word) {
      queue.unshift(word);
      word = "";
    } else if (char !== " ") {
      word += char;
    }
    left++;
  }
  queue.unshift(word);
  return queue.join(" ");
};

// 测试结果:
reverseWords("the sky is blue"); // "blue is sky the"
reverseWords("  hello world!  "); // "world! hello"
reverseWords("a good   example"); // "example good a"

给定一个整数数组 nums 和一个目标值 target ,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

解题思路:

var twoSum = function (arr, target) {
  const map = new Map();
  for (let i = 0; i < arr.length; i++) {
    let k = target - arr[i];
    if (map.has(k)) {
      return [map.get(k), i];
    }
    map.set(arr[i], i);
  }
  return [];
};

// 测试结果:
var arr = [2, 7, 11, 15];
var target = 9;
twoSum(arr, target); // [0, 1]

更多内容参考字节&leetcode1:两数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a ,b ,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

示例:

给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
  [-1, 0, 1],
  [-1, -1, 2]
]
//这道题经典的是细节,需要考虑蛮多细节的
//解法:
//1.暴力破解,三层枚举,O(n^3)效率太低不考虑
//2.a+b+c = 0,转换思路,a+b = -c,这不就是两数之和嘛?
//3.利用双指针夹逼,但是怎么避免重复呢?
//3.1 排序即可,利用排序好的数组,固定一个指针i,夹逼选举left和right
//3.2 这道题难度在于要考虑的细节太多,多想想即可。
//3.3 扩展一下,如果是4数之和,五数之和,N数之和呢?怎么解决?
const threeSum = (nums) => {
  const len = nums.length;
  const result = [];
  // 因为是三数之和,小于三个不用考虑了
  if (len < 3) {
    return result;
  }
  nums.sort((a, b) => a - b);
  // len - 2 同样道理,小于三个不用考虑
  for (let i = 0; i < len - 2; i++) {
    // 如果第一个就大于0了,三个加起来肯定大于0
    if (nums[i] > 0) {
      break;
    }
    // 避免掉重复的情况
    if (i && nums[i] === nums[i - 1]) {
      continue;
    }
    let left = i + 1;
    let right = len - 1;
    // 双指针夹逼
    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];
      if (sum === 0) {
        result.push([nums[i], nums[left++], nums[right--]]);
        // push 加了之后防止还存在重复
        while (nums[left] === nums[left - 1]) {
          left++;
        }
        while (nums[right] === nums[right + 1]) {
          right--;
        }
      } else if (sum > 0) {
        right--;
      } else {
        left++;
      }
    }
  }
  return result;
};

更多解答参考腾讯&leetcode15:三数之和

洗牌算法

/**
 * 洗牌算法:
 * 1. 生成一个0到arr.length的随机数
 * 2. 交换该随机数位置元素和数组的最后一个元素,并把改随机位置的元素放入结果数组中
 * 3. 生成一个0~arr.length-1的随机数
 * 4. 交换该随机数位置元素和数组的倒数第二个元素,并把该随位置的元素放入结果数组中
 * 以此类推,直至取完所需的n个元素
 */
function xipai(arr, size) {
  let result = [];
  for (let i = 0; i < size; i++) {
    let index = Math.floor(Math.random() * (arr.length - i));
    result.push(arr[index]);
    [arr[index], arr[arr.length - i - 1]] = [
      arr[arr.length - i - 1],
      arr[index],
    ];
  }
  return result;
}
赞 赏