做五道算法题 Ⅰ

240 阅读6分钟

1、最大子序和

题目

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

思路

  • 动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans

  • 如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字

  • 如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字

  • 每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果

  • 时间复杂度:O(n)O(n)

题解

var maxSubArray = function(nums) {
    let ans = nums[0];
    let sum = 0;
    for(const num of nums) {
        if(sum > 0) {
            sum += num;
        } else {
            sum = num;
        }
        ans = Math.max(ans, sum);
    }
    return ans;
};

2、Excel表列名称

题目

给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。

思路

这是一道从 11 开始的的 2626 进制转换题。

对于一般性的进制转换题目,只需要不断地对 columnNumbercolumnNumber 进行 % 运算取得最后一位,然后对 columnNumbercolumnNumber 进行 / 运算,将已经取得的位数去掉,直到 columnNumbercolumnNumber 为 00 即可。

一般性的进制转换题目无须进行额外操作,是因为我们是在「每一位数值范围在 [0,x)[0,x)」的前提下进行「逢 xx 进一」。

但本题需要我们将从 11 开始,因此在执行「进制转换」操作前,我们需要先对 columnNumbercolumnNumber 执行减一操作,从而实现整体偏移。

题解

var convertToTitle = function(columnNumber) {
  let sb = '';
  while(columnNumber > 0){
      columnNumber --;
      sb = sb + String.fromCharCode(columnNumber%26 + 'A'.charCodeAt(0));
      columnNumber = Math.floor(columnNumber/26);
  }
  sb = sb.split('').reverse().join('');
  return sb;
 };

3、实现Trie(前缀树)

题目


Trie
(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word 。
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。

思路

前缀树

(只保存小写字符的)「前缀树」是一种特殊的多叉树,它的 TrieNode 中 chidren 是一个大小为 26 的一维数组,分别对应了26个英文字符 'a' ~ 'z',也就是说形成了一棵 26叉树。

前缀树的结构可以定义为下面这样。 里面存储了三个信息:

value 表示当前的单词就是当前的value值。 children 是该节点的所有子节点。 end 表示当前节点是否是单词的结束。

class Node {
  constructor(value){
    this.value = value;
    this.children = {};
    this.end = false;
  }
}

构建

在构建前缀树的时候,按照下面的方法:

根节点不保存任何信息; 关键词放到「前缀树」时,需要把它拆成各个字符,每个字符按照其在 'a' ~ 'z' 的序号,放在对应的 chidren 里面。下一个字符是当前字符的子节点。 一个输入字符串构建「前缀树」结束的时候,需要把该节点的 isWord 标记为 true,说明从根节点到当前节点的路径,构成了一个关键词。 下面是一棵「前缀树」,其中保存了 {"am", "an", "as", "b", "c", "cv"} 这些关键词。图中红色表示 isWord 为 true。 看下面这个图的时候需要注意:

所有以相同字符开头的字符串,会聚合到同一个子树上。比如 {"am", "an", "as"} ; 并不一定是到达叶子节点才形成了一个关键词,只要 isWord 为true,那么从根节点到当前节点的路径就是关键词。比如 {"c", "cv"} ;

image.png

有些题解把字符画在了节点中,我认为是不准确的。因为前缀树是根据 字符在 children 中的位置确定子树,而不真正在树中存储了 'a' ~ 'z' 这些字符。树中每个节点存储的 isWord,表示从根节点到当前节点的路径是否构成了一个关键词。

查询

在判断一个关键词是否在「前缀树」中时,需要依次遍历该关键词所有字符,在前缀树中找出这条路径。可能出现三种情况:

在寻找路径的过程中,发现到某个位置路径断了。比如在上面的前缀树图中寻找 "d" 或者 "ar" 或者 "any" ,由于树中没有构建对应的节点,那么就查找不到这些关键词; 找到了这条路径,但是最后一个节点的 isWord 为 false。这也说明没有改关键词。比如在上面的前缀树图中寻找 "a" ; 找到了这条路径,并且最后一个节点的 isWord 为 true。这说明前缀树存储了这个关键词,比如上面前缀树图中的 "am" , "cv" 等。 应用 上面说了这么多前缀树,那前缀树有什么用呢?

其实我们生活中就有应用。比如我们常见的电话拨号键盘,当我们输入一些数字的时候,后面会自动提示以我们的输入数字为开头的所有号码。

比如我们的英文输入法,当我们输入半个单词的时候,输入法上面会自动联想和补全后面可能的单词。

题解

class Trie {
  constructor(){
    this.root = new Node()
  }
  insert(word){
    let currentNode = this.root;
    for (const c of word) {
      if (!currentNode.children[c]) {
        currentNode.children[c] = new Node(c);
      }
      currentNode = currentNode.children[c];
    }
    currentNode.end = true;
  }
  startsWith(word){
    let currentNode = this.root;
    for(const c of word) {
      if(!currentNode.children[c]){
        return false;
      }
      currentNode = currentNode.children[c];
    }
    return true;
    
  }
  search(word){
    let currentNode = this.root;
    for(const c of word) {
      if(!currentNode.children[c]){
        return false;
      }
      currentNode = currentNode.children[c];
    }
    return currentNode.end;
  }
}

4、删除排序列表中的重复元素

题目

存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。 返回同样按升序排列的结果链表。

思路

  • 标签:链表
  • 指定 cur 指针指向头部 head
  • 当 cur 和 cur.next 的存在为循环结束条件,当二者有一个不存在时说明链表没有去重复的必要了
  • 当 cur.val 和 cur.next.val 相等时说明需要去重,则将 cur 的下一个指针指向下一个的下一个,这样就能* 达到去重复的效果
  • 如果不相等则 cur 移动到下一个位置继续循环
  • 时间复杂度:O(n)O(n)

题解

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
  let cur = head;
  while(cur!= null && cur.next != null){
      if(cur.val === cur.next.val){
          cur.next = cur.next.next;
      }else{
          cur = cur.next
      }
  }
  return head;
};

5、回文对

题目

给定一组 互不相同 的单词, 找出所有 不同** 的索引对 (i, j),使得列表中的两个单词, words[i] + words[j] ,可拼接成回文串。

思路

这道题的暴力解非常简单,两层for循环,把字符串拼接一起判断是不是回文即可,一些写C类语言的胖友说自己的暴力拆解会导致超时,答题不通过,但是我用JS写了之后发现还可以,击败了16%的人,官方的其他前缀树和哈希表解法有些烧脑,就暂时先不展示啦

题解

/**
 * @param {string[]} words
 * @return {number[][]}
 */
var palindromePairs = function(words) {
    let result = [];
    for(let i = 0;i<words.length;i++){
        for(let j = 0;j<words.length;j++){
            if(i===j){
                continue
            }
            if(is_palindromePairs(words[i]+words[j])){
                result.push([i,j])
            }
        }
    }
    return result;
}; 

const is_palindromePairs = function(s){
    for(let i = 0;i<s.length/2;i++){
        if(s[i]!= s[s.length-1-i]){
           return false
        }
    }
    return true;
}