首发于微信公众号《前端成长记》,写于 2020.05.07

背景

本文记录刷题过程中的整个思考过程,以供参考。主要内容涵盖:

  • 题目分析设想
  • 编写代码验证
  • 查阅他人解法
  • 思考总结

目录

Easy

155.最小栈

题目地址

题目描述

设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) -- 将元素 x 推入栈中。
  • pop() -- 删除栈顶的元素。
  • top() -- 获取栈顶元素。
  • getMin() -- 检索栈中的最小元素。

示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.

题目分析设想

这道题感觉就是构造函数,仅此而已,给函数的原型加一些方法。差异就在方法实现本身上,这个就写一个个人的思路吧,内部维护每次操作后的最小值数组。当然用差值数组和最小值也是可以的。

编写代码验证

Ⅰ.内部维护最小值数组

代码:

/**
* initialize your data structure here.
*/
var MinStack = function() {
// 栈内数据用数组存储
this.stacks = []
this.mins = []
}; /**
* @param {number} x
* @return {void}
*/
MinStack.prototype.push = function(x) {
this.stacks.push(x)
// 比当前最小值小的数都需要存住
if (!this.mins.length || this.mins[this.mins.length - 1] >= x) {
this.mins.push(x)
}
}; /**
* @return {void}
*/
MinStack.prototype.pop = function() {
// 如果推出的是最小值,那最小值数组也同步更新即可
if (this.stacks.pop() === this.mins[this.mins.length - 1]) {
this.mins.pop()
}
}; /**
* @return {number}
*/
MinStack.prototype.top = function() {
return this.stacks.length ? this.stacks[this.stacks.length - 1] : undefined
}; /**
* @return {number}
*/
MinStack.prototype.getMin = function() {
return this.mins[this.mins.length - 1]
};

结果:

  • 18/18 cases passed (116 ms)
  • Your runtime beats 89.63 % of javascript submissions
  • Your memory usage beats 7.49 % of javascript submissions (44.9 MB)
  • 时间复杂度: O(1)

Ⅱ.差值数组和最小值

代码:

/**
* initialize your data structure here.
*/
var MinStack = function() {
// 栈内数据用数组存储
this.stacks = []
this.min = Infinity
}; /**
* @param {number} x
* @return {void}
*/
MinStack.prototype.push = function(x) {
if (!this.stacks.length) {
this.min = x
// x - this.min = 0
this.stacks.push(0)
} else {
// 先存差值再更新最小值
this.stacks.push(x - this.min)
if (x < this.min) this.min = x
}
}; /**
* @return {void}
*/
MinStack.prototype.pop = function() {
// 当前值比最小值小,则需要
let val = this.stacks.pop()
if (val < 0) {
this.min -= val
}
}; /**
* @return {number}
*/
MinStack.prototype.top = function() {
if (this.stacks[this.stacks.length - 1] < 0) {
// 当前最小值就是推出项的值
return this.min
} else {
return this.stacks[this.stacks.length - 1] + this.min
}
}; /**
* @return {number}
*/
MinStack.prototype.getMin = function() {
return this.min
};

结果:

  • 18/18 cases passed (124 ms)
  • Your runtime beats 71.47 % of javascript submissions
  • Your memory usage beats 16.67 % of javascript submissions (44.5 MB)
  • 时间复杂度: O(1)

查阅他人解法

这里看到有用栈的,JS 里面我就不构建了。另外还看见一种把值和最小值混合存储的思路,挺有意思的。

Ⅰ.混合存储

代码:

/**
* initialize your data structure here.
*/
var MinStack = function() {
// 栈内数据用数组存储
this.stacks = []
this.min = Infinity
}; /**
* @param {number} x
* @return {void}
*/
MinStack.prototype.push = function(x) {
// 先推上一步的最小值,再推原数据
if (x <= this.min) {
this.stacks.push(this.min)
this.min = x
}
this.stacks.push(x)
}; /**
* @return {void}
*/
MinStack.prototype.pop = function() {
// 如果推出值等于当前最小值,则将最小值更新为上一个最小值
if (this.stacks.pop() === this.min) {
this.min = this.stacks.pop()
}
}; /**
* @return {number}
*/
MinStack.prototype.top = function() {
return this.stacks[this.stacks.length - 1]
}; /**
* @return {number}
*/
MinStack.prototype.getMin = function() {
return this.min
};

结果:

  • 18/18 cases passed (124 ms)
  • Your runtime beats 71.47 % of javascript submissions
  • Your memory usage beats 7.15 % of javascript submissions (45 MB)
  • 时间复杂度: O(1)

思考总结

我个人是不推荐混合存储的,因为如果需要拓展查最大值,那整体逻辑实际上都需要调整。而前面两种方法,都只需要增加最大值数组或者最大值即可。

160.相交链表

题目地址

题目描述

编写一个程序,找到两个单链表相交的起始节点。

如下面的两个链表:

在节点 c1 开始相交。

注意:

  • 如果两个链表没有交点,返回 null.
  • 在返回结果后,两个链表仍须保持原有的结构。
  • 可假定整个链表结构中没有循环。
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

题目分析设想

这道题如果不加限制的话,方法还是很多的,比如说打标识,或者哈表表都可以解决问题。如果要满足内存要求的话,必然就不能使用哈希表。要满足保持原有结构,那就不能用打标识的方法了。这里有个取巧的方案,因为不存在循环,两者相加的总长度相等,所以如果存在交点,那结尾必然相等。

编写代码验证

Ⅰ.打标识

代码:

/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
// 因为是内存引用指向,取巧了
while(headA) {
headA.FLAG = true
headA = headA.next
}
while(headB) {
if (headB.FLAG) return headB
headB = headB.next
}
return null
};

结果:

  • 45/45 cases passed (104 ms)
  • Your runtime beats 50.31 % of javascript submissions
  • Your memory usage beats 5.01 % of javascript submissions (45.3 MB)
  • 时间复杂度: O(m + n)

Ⅱ.哈希表法

代码:

/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
let hash = new Set()
while(headA) {
hash.add(headA)
headA = headA.next
}
while(headB) {
if (hash.has(headB)) return headB
headB = headB.next
}
return null
};

结果:

  • 45/45 cases passed (100 ms)
  • Your runtime beats 67.12 % of javascript submissions
  • Your memory usage beats 68.1 % of javascript submissions (43.8 MB)
  • 时间复杂度: O(m + n)

Ⅲ.双指针法

代码:

/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
// 用两个指针同时右移,A完了之后指向B继续右移
let pA = headA
let pB = headB
while(pA !== pB) {
pA = pA ? pA.next : headB
pB = pB ? pB.next : headA
}
return pA
};

结果:

  • 45/45 cases passed (100 ms)
  • Your runtime beats 67.12 % of javascript submissions
  • Your memory usage beats 72.7 % of javascript submissions (43.3 MB)
  • 时间复杂度: O(m + n)

查阅他人解法

这里一般的二重循环就不说了,跟两个数组中找同样的数一样,没有什么区别,效率也比较底下。另外看见一个有意思的思路是,把两个链表拼成环,转化成找环节点的问题。

Ⅰ.转换环

代码:

/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
if (headA == null || headB == null) return null;
let curA = headA,curB = headB;
while(curA.next != null){
curA = curA.next;
}
curA.next = headA;
let fast = headB,slow = headB;
while(fast != null&&fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
slow = headB;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
curA.next = null;
return fast;
}
}
curA.next = null;
return null;
};

结果:

  • 45/45 cases passed (104 ms)
  • Your runtime beats 50.31 % of javascript submissions
  • Your memory usage beats 12.39 % of javascript submissions (44.6 MB)
  • 时间复杂度: O(m + n)

思考总结

这里要满足空间复杂度要求,那就不能用哈希表法;要满足不改变原始数据结构,那就不能用标识法。所以这种情况下我更建议使用双指针法来直接作答,而转换为环虽然是一个思路,但是其实由一个问题转换为另一个问题,心智负担没有降低。

167.两数之和II输入有序数组

题目地址

题目描述

给定一个已按照 升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。

函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。

说明:

  • 返回的下标值(index1 和 index2)不是从零开始的。
  • 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。

示例:

输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

题目分析设想

这道题有几个地方做了说明了,需要注意一下:有序的升序数组、下标不从零开始、不能重复使用相同元素。之前第一期的两数之和的方法就不做分析了(两次遍历和哈希表),这里可以利用有序数组来提高效率,利用首尾指针,来做判断,可以理解为夹逼原则。

编写代码验证

Ⅰ.双指针夹逼

代码:

/**
* @param {number[]} numbers
* @param {number} target
* @return {number[]}
*/
var twoSum = function(numbers, target) {
let start = 0
let end = numbers.length - 1
while(start < end) {
const sum = numbers[start] + numbers[end]
if (sum === target) {
// 不从零开始,所以需要加一
return [start + 1, end + 1]
} else if (sum > target) {
end--
} else {
start++
}
}
// 由于有唯一输出,所以必然有结果,也不需要做特殊处理
return [-1, -1]
};

结果:

  • 17/17 cases passed (64 ms)
  • Your runtime beats 87.01 % of javascript submissions
  • Your memory usage beats 40.91 % of javascript submissions (35.2 MB)
  • 时间复杂度: O(n)

查阅他人解法

看了一下解法,没有效率更高的方式了。一些基本的解法在第一期的第一题可以查阅。另外有一个是以一个数为基准,二分查找,但是这样的话其实只是在两次循环基础上做了优化,甚至还不如哈希表效率高。所以,这里就没有看见其他有不同思路的解法了。

思考总结

既然数组有序,我们就可以用夹逼原则来求解,在这道题我觉得是最合适,也是最高效的解法了。

168.Excel表列名称

题目地址

题目描述

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

例如,

    1 -> A
2 -> B
3 -> C
...
26 -> Z
27 -> AA
28 -> AB
...

示例:

输入: 1
输出: "A" 输入: 28
输出: "AB" 输入: 701
输出: "ZY"

题目分析设想

这道题其实很清晰,就是将10进制转成26进制的问题。直接用取余运算就行,唯一的难点在于如何转成26进制。

编写代码验证

Ⅰ.取余

代码:

/**
* @param {number} n
* @return {string}
*/
var convertToTitle = function(n) {
let str = ''
while( n > 0) {
// 因为A代表1,减一就能从0开始匹配进制转换
n--
str += String.fromCharCode(n % 26 + 'A'.charCodeAt())
n = Math.floor(n / 26)
}
return str.split('').reverse().join('')
};

结果:

  • 18/18 cases passed (64 ms)
  • Your runtime beats 57.06 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (33.7 MB)
  • 时间复杂度: O(n)

查阅他人解法

看了一下解法,思路没有什么区别,有一个有意思的就是强行转换成一行,这里也列一下。

Ⅰ.单行代码

代码:

/**
* @param {number} n
* @return {string}
*/
var convertToTitle = function(n, s = '') {
return !n-- ? s : convertToTitle(~~(n / 26), String.fromCharCode('A'.charCodeAt() + n % 26) + s);
};

结果:

  • 18/18 cases passed (60 ms)
  • Your runtime beats 76.47 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (33.7 MB)
  • 时间复杂度: O(n)

思考总结

这道题本质上就是一道进制转换的题目,没有什么太大的难度。

169.求众数

题目地址

题目描述

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例:

输入: [3,2,3]
输出: 3 输入: [2,2,1,1,1,2,2]
输出: 2

题目分析设想

这道题就是找出出现次数最多的元素,按照常规的思路的话有这么两种方式。

  • 哈希法,通过哈希表记录每个数出现的次数,找到出现次数最多的数
  • 分治法,将数组二分,分别求两边的多数元素,然后找出出现次数多的元素

由于这里是找多数元素,有个前提是保证出现次数大于一半,这里也可以取巧,先排序,排序后中间那个数一定是多数元素。

编写代码验证

Ⅰ.哈希法

代码:

/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
let hash = {}
for(let i = 0; i < nums.length; i++) {
if (hash[nums[i]] === undefined) {
hash[nums[i]] = 1
} else {
hash[nums[i]] += 1
// 大于等于一半就可以直接认定为众数了
if (hash[nums[i]] >= nums.length / 2) {
return nums[i]
}
}
}
let count = 0
let val = null
for(let key in hash) {
if (hash[key] > count) {
count = hash[key]
val = key
}
}
return val
};

结果:

  • 46/46 cases passed (88 ms)
  • Your runtime beats 41.1 % of javascript submissions
  • Your memory usage beats 92.86 % of javascript submissions (37.6 MB)
  • 时间复杂度: O(n)

Ⅱ.分治法

代码:

/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
function countEle(arr, num, l, h) {
let count = 0
for(let i = l; i <= h; i++) {
if (arr[i] === num) {
count += 1
}
}
return count
}
function getEle(arr, l, h) {
debugger
if (l === h) { // 就一个元素,直接返回
return arr[l]
}
// 取中位数,减法主要为了防止溢出
let mid = parseInt((h - l) / 2 + l)
// 左边的众数
let left = getEle(arr, l, mid)
// 右边的众数
let right = getEle(arr, mid + 1, h)
// 如果左右两边数组为同一个众数,则直接返回
if (left === right) {
return left
}
// 比较众数出现次数,注意:要用父数组取出现次数做比较
// 要不像这种将返回错误结果:[4,5,4,4,4,5]
let leftCount = countEle(arr, left, l, h)
let rightCount = countEle(arr, right, l, h)
return leftCount > rightCount ? left : right
}
return getEle(nums, 0, nums.length - 1)
};

结果:

  • 46/46 cases passed (148 ms)
  • Your runtime beats 5.98 % of javascript submissions
  • Your memory usage beats 28.57 % of javascript submissions (38.2 MB)
  • 时间复杂度: O(nlog(n))

Ⅲ.排序法

代码:

/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
return nums.sort()[nums.length >>> 1]
};

结果:

  • 46/46 cases passed (100 ms)
  • Your runtime beats 24.58 % of javascript submissions
  • Your memory usage beats 92.86 % of javascript submissions (37.3 MB)
  • 时间复杂度: O(nlog(n)),完全是数组排序的复杂度

查阅他人解法

发现了一种叫做 Boyer Moore 投票算法。这个算法通俗点解释起来还是很容易理解的,其实可以理解为互相抵消。每一个众数和一个其他数抵消,剩下的必然就是众数了。

Ⅰ.投票算法

代码:

/**
* @param {number[]} nums
* @return {number}
*/
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
let count = 0
let candidate = null
for(let i = 0; i < nums.length; i++) {
if (count === 0) {
candidate = nums[i]
}
// 同样的数累加,不同的数相减,可以理解为同数量抵消,抵消完产生新的备选数
count += (nums[i] === candidate) ? 1 : -1
}
return candidate
};

结果:

  • 46/46 cases passed (80 ms)
  • Your runtime beats 56.39 % of javascript submissions
  • Your memory usage beats 92.86 % of javascript submissions (37.3 MB)
  • 时间复杂度: O(n)

思考总结

比较显而易见的是,这道题使用投票算法只需遍历一次,也不需要额外的空间,为最优解。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验

如果能给您带去些许帮助,欢迎 ️star 或 ️ fork

(转载请注明出处:https://chenjiahao.xyz)

【Leetcode 做题学算法周刊】第八期的更多相关文章

  1. 【Leetcode 做题学算法周刊】第四期

    首发于微信公众号<前端成长记>,写于 2019.11.21 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 67 ...

  2. 【Leetcode 做题学算法周刊】第一期

    首发于微信公众号<前端成长记>,写于 2019.10.28 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 1. ...

  3. 【Leetcode 做题学算法周刊】第二期

    首发于微信公众号<前端成长记>,写于 2019.11.05 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 20 ...

  4. 【Leetcode 做题学算法周刊】第六期

    首发于微信公众号<前端成长记>,写于 2019.12.15 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 11 ...

  5. 【Leetcode 做题学算法周刊】第七期

    首发于微信公众号<前端成长记>,写于 2020.01.15 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 12 ...

  6. 【Leetcode 做题学算法周刊】第三期

    首发于微信公众号<前端成长记>,写于 2019.11.13 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 35 ...

  7. 【Leetcode 做题学算法周刊】第五期

    首发于微信公众号<前端成长记>,写于 2019.12.06 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 10 ...

  8. C#LeetCode刷题-贪心算法

    贪心算法篇 # 题名 刷题 通过率 难度 44 通配符匹配   17.8% 困难 45 跳跃游戏 II   25.5% 困难 55 跳跃游戏   30.6% 中等 122 买卖股票的最佳时机 II C ...

  9. C#LeetCode刷题-分治算法

    分治算法篇 # 题名 刷题 通过率 难度 4 两个排序数组的中位数 C#LeetCode刷题之#4-两个排序数组的中位数(Median of Two Sorted Arrays)-该题未达最优解 30 ...

随机推荐

  1. 服务注册与发现【Eureka】- Eureka简介

    什么是服务治理 SpringCloud 封装了 Netflix 公司开发的 Eureka 模块来 实现服务治理. 在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所 ...

  2. metaspolit下UAC提权以及日志清除

    在获得webshell时但权限不够大,这时候为了完全获得受害者机器的权限,使用msf进行后渗透. 一.获取Meterpreter会话 Meterpreter 是msf的一个payload,目标执行之后 ...

  3. 【mysql】- 索引简介篇

    简介 我们都知道mysql使用存储引擎的是InnoDB,InnoDB使用的索引的对应的数据结构是B+树 结构图: 如上图所示,我们实际用户记录是存放在B+树的最底层的节点上,这些节点也被称为叶子节点或 ...

  4. Java中static、final和static final(final static)的区别(转)

    大佬的总结(大赞!) final可以修饰:属性,方法,类,局部变量(方法中的变量) final修饰的属性的初始化可以在编译期,也可以在运行期,初始化后不能被改变. final修饰的属性跟具体对象有关, ...

  5. 阿里云OSS服务器的使用

    关于文件上传,我们一般使用OSS服务器.大致为两种上传方式: 详情官网参考:https://help.aliyun.com/document_detail/31927.html?spm=a2c4g.1 ...

  6. jQuery与javascript

    jQuery 是一个 JavaScript 库,jQuery 极大地简化了 JavaScript 编程. javaScript(js)和jQuery(jq) 都是找元素.操作元素 Dom操作的区别: ...

  7. dp最长不下降序列

    // // Created by snnnow on 2020/4/13. // //这是dp 问题的基础题 // //最长不下降 //(导弹拦截是其例题) //那这篇文章是讲啥呢, // 主要是吧, ...

  8. 在excel中如何给一列数据批量加上双引号

    在实际开发中,会遇到这样的需求,大量的数据,需要从配置文件里读取,客户给到的枚举值是字符串,而配置文件里的数据,是json格式,需要加上双引号,这样就需要使用Excel来批量格式化一下数据. 客户给到 ...

  9. PHP mysqli_real_escape_string() 函数

    转义字符串中的特殊字符: <?php 高佣联盟 www.cgewang.com // 假定数据库用户名:root,密码:123456,数据库:RUNOOB $con=mysqli_connect ...

  10. C/C++编程笔记:C语言NULL值和数字 0 值区别及NULL详解

    在学习C语言的时候,我们常常会碰到C语言NULL值和数字 0 ,很多小伙伴搞不清楚他们之间的一个区别,今天我们就了解一下他们之间的区别,一起来看看吧! 先看下面一段代码输出什么: 输出<null ...