时间复杂度
算法的时间复杂度并不是要反映每行代码到底执行了多少次,而是反映代码执行总次数的一个趋势;所以通常,若O(n)是常数则简化为1;若O(n)是多项式,比如5n^2+5n就简化为O(n^2),只保留次数最高的项
对于多层遍历来说,我们只关心最内层的循环体被执行了多少次
// 在for循环中,判断语句会比递增语句多执行一次
/**
* let i=0;只会执行一次
* i<arr.length 判断语句执行n+1次
* i++ 递增语句执行n次
* 循环体执行n次
**/
for(let i=0;i<arr.length;i++){}
一般有几层循环时间复杂度就是n的多少次方;如果递增项是跳跃的比如下面的循环,当i=i*2递增了x次后i就会大于len即2^x>n,所以x的执行次数就是log2n被简化之后就是logn
for(let i=0; i<len;i= i*2){console.log(arr[i])};
空间复杂度
描述内存的增长趋势,占内存的就是我们的变量和参数
下面方法中占内存的是n、i和arr;n和i不会随着函数的执行占更多的内存,但是arr的长度会随着循环的执行而增加,所以空间复杂度为O(n)
function init(n) {
var arr = []
for(var i=0;i<n;i++) {
arr[i] = i
}
return arr
}
数组的应用
两数求和问题:
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标
示例: 给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
方法一:两层循环,第一层循环的值和第二层循环的值相加等于target即可
function twoSum(nums,target) {
const len = nums.length;
for(let i=0;i<len;i++){
for(let j=0;j<len;j++){
// i和j不相等且两数相加和为target即可
if(i !== j && nums[i] + nums[j] === target){
return [i,j]
}
}
}
}
console.log(twoSum([2, 7, 11, 15],9))
方法二:用空间换时间,使用一个map对象记录遍历过的索引和元素,在后面的遍历中去找当前值和target的差值是否在map对象中,如果在返回两个下标即可
function twoSum(nums,target) {
const len = nums.length;
const obj = {[nums[0]]:0}
for(let i=1;i<len;i++){
if(obj[target-nums[i]] !== undefined){
return [obj[target-nums[i]],i]
}
obj[nums[i]] = i;
}
}
console.log(twoSum([2, 7, 11, 15],9))
合并两个有序数组:
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
示例: 输入: nums1 = [1,2,3,0,0,0], m = 3 nums2 = [2,5,6], n = 3 输出: [1,2,2,3,5,6]
思路:使用双指针实现,将两个指针分别指向两个生效数组的尾部,计算出合并后数组arr的长度,比较两个数组的值的大小依次往arr数组的尾部填充即可
function threeSum(nums1,m,nums2,n){
// 初始化指针,i指向数组nums的最后一个元素;j指向nums2的最后一个元素;k表示合并后数组的最后一个元素
// 都是排序的,所以比较两个数组谁大就往后放就行
let i = m - 1,j=n-1,k=m+n-1;
// 两个数组都有值时
while(i>0 && j>0){
if(nums2[j] >= nums1[i] ){
nums1[k] = nums2[j];
j--;
k--;
}
if(nums2[j] < nums1[i] ){
nums1[k] = nums1[i];
i--;
k--;
}
}
// 如果nums2还有值还需要放到nums1中去
while(j>=0){
nums1[k] = nums2[j];
j--;
k--;
}
}
三数求和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组
思路:循环的过程中,每循环到一个元素,用双指针指向当前元素的下一个元素和数组的最后一个元素,指针往中间靠拢,直到找到三数之和为目标元素即可停止循环
function threeSum(nums){
// 先对nums排序
nums = nums.sort((a,b)=>a-b);
// 存放结果数组
const res = [];
const len = nums.length;
for(let i=0;i<len - 2;i++){
let j = i+1;
let k = len - 1;
// 如果遇到重复的数字,则跳过
if(i>0&&nums[i]===nums[i-1]) {
continue
}
while(j<k){
// 如果三数之和小于0,j往后移动;如果三数之和大于0,k往前移动
if(nums[i] + nums[j] + nums[k] < 0){
j++;
// 处理左指针元素重复的情况
while(j<k && nums[j]===nums[j-1]) {
j++;
}
}else if(nums[i] + nums[j] + nums[k] >0){
k--;
while(j<k && nums[k] === nums[k-1]){
k--;
}
}else {
// 得到目标数字组合,推入结果数组
res.push([nums[i],nums[j],nums[k]])
// 左右指针一起前进
j++;
k--;
// 若左指针元素重复,跳过
while(j<k&&nums[j]===nums[j-1]) {
j++;
}
// 若右指针元素重复,跳过
while(j<k&&nums[k]===nums[k+1]) {
k--;
}
}
}
}
return res;
}
字符串的应用
算法基本技能
反转字符串:
// 定义被反转的字符串
const str = 'juejin'
// 定义反转后的字符串
const res = str.split('').reverse().join('')
console.log(res) // nijeuj
回文字符串:
解释:正着读和反着读是一样的;或者说从中间分开得到两个字符串,第一个字符串从左到右和第二个字符串从右到左完全是一样的
function isPalindrome(str) {
// 先反转字符串
const reversedStr = str.split('').reverse().join('')
// 判断反转前后是否相等
return reversedStr === str
}
function isPalindrome(str) {
const len = str.length;
for(let i = 0;i < len/2; i++){
if(str[i] !== str[len - i - 1]){
return false
}
}
return true;
}
回文字符串的衍生问题:
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串
方法一:先判断字符串本身是否是回文字符串,如果不是遍历整个字符串,遍历到的元素挨个删除看剩下的字符串是否是回文字符串
function validPalindrome(str){
if(isPalindrome(str)) return true;
const len = str.length;
for(let i = 0; i<len; i++){
const newStr = str.substring(0,i) + str.substring(i+1,len);
if(isPalindrome(newStr)) return true;
}
return false;
}
方法二:双指针,一个指针指向字符串的头部,一个指针指向字符串的尾部,如果相等符合回文字符串,如果不等,移动一个指针看剩下的是否是回文字符串
function validPalindrome(str){
const len = str.length;
// 定义两个指针
let i = 0;j = len - 1;
while(str[i] === str[j]){
i++;
j--;
}
// 移动一个指针,剩下的是否是回文字符串
if(isPalindrome(i+1,j)){
return true;
}
// 移动一个指针,剩下的是否是回文字符串
if(isPalindrome(i,j+1)){
return true;
}
return false;
}
字符串匹配问题:
设计一个支持以下两种操作的数据结构:
void addWord(word)
bool search(word)
search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a-z ;. 可以表示任何一个字母。
分析:
- 包含addWord和searchWord方法所以是构造函数
- addWord是添加字符,searchWord是查找字符;js中查找最快的就是对象,所以添加字符就添加到对象即可
- 可以以每个字符串为key;也可以将相同字符串长度的字符串放到一个数组中,key为字符串长度,可以快速定位
- 可以看到以点为占位,点可以代表任何的字母,但是总长度一定是代表的当前字符串的长度
const WordDictionary = function () {
// 初始化一个对象字面量,承担 Map 的角色
this.words = {}
};
WordDictionary.prototype.addWords = function (word) {
const len = word.length;
if(!this.words[len]){
this.words[len] = [];
};
this.words[len].push(word);
}
WordDictionary.prototype.searchWords = function (word){
const len = word.length;
// 如果没有在words对象中找到数组就肯定没有对应的字符串
if(!this.words[len]){
return false;
}
const wordArr = this.words[len];
// 判断是不是纯字符串
if(!word.includes('.')){
// 找到包含字符串长度的数组看是否包含word
return wordArr.includes(word);;
}
// 不是纯字符串的话就需要匹配正则
const reg = new RegExp(word);
return wordArr.some((item)=>{
return reg.test(item);
})
}
字符串与数字之间的转换
请你来实现一个 atoi 函数,使其能将字符串转换成整数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。 当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。 该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0。
说明:假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−2^31, 2^31 − 1]。如果数值超过这个范围,请返回 INT_MAX (2^31 − 1) 或 INT_MIN (−2^31)
示例 : 输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。
示例 : 输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。 因此无法执行有效的转换。
示例 :
输入: "-91283472332"
输出: -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。因此返回 INT_MIN (−2^31) 。
分析:
1、第一个非空字符,表示我们要先对字符串前后去空格
2、第一个字符可以是符号,说明第一个字符除了数字之外,可以是'-'或者'+'
3、多余的字符可以忽略,说明遇到字符就不再往后查找
4、边界问题,需要找到最大和最小值
// 计算最大值
const max = Math.pow(2,31) - 1;
// 计算最小值
const min = -max - 1;
方法一:遍历整个字符串将上述的四点作为判断条件即可
function myAtoi(str){
let numStr = '';
const newStr = str.trim(); // 先对字符串去除前后空格
const max = Math.pow(2,31) - 1;
const min = -max - 1;
const firstWord = newStr.substring(0,1);
// isNaN会将字符串转换为数字再判断
if(firstWord && (firstWord === '+' || firstWord === '-' || !isNaN(firstWord))){
const len = newStr.length;
for(let i = 1; i < len; i++){
if(newStr[i].trim() && !isNaN(newStr[i])){
numStr += newStr[i];
}else {
break;
}
}
numStr = Number(firstWord + numStr);
// 边界判断
if(isNaN(numStr)){
return 0;
}
if(numStr > max) {
return max
} else if( numStr < min) {
return min
}
// 返回转换结果
return numStr
}
return 0;
}
方法二:用正则捕获匹配字符
1、需要匹配之后的数字,所以需要捕获,就是()的部分
2、\s*,表示匹配0个或多个空格,这是不需要被捕获的
3、[]表示里面的内容为可选或,可以没有可以只有一个
4、然后就是数字[0-9]*表示任意多个数字,以上就是捕获组中的内容
5、.*表示匹配任意字符,这就不是我们关心的了
特别注意:如果正则尾部有g,match()会返回与完整正则表达式匹配的所有结果;如果没有g,只会返回第一个完整匹配的捕获组
const myAtoi = function(str) {
// 编写正则表达式
const reg = /\s*([-\+]?[0-9]*).*/;
// 得到捕获组
const groups = str.match(reg);
const max = Math.pow(2,31) - 1;
const min = -max - 1;
// targetNum 用于存储转化出来的数字
let targetNum = 0;
// 如果匹配成功
if(groups) {
// 尝试转化捕获到的结构
targetNum = +groups[1];
// 注意,即便成功,也可能出现非数字的情况,比如单一个'+'
if(isNaN(targetNum)) {
// 不能进行有效的转换时,请返回 0
targetNum = 0;
}
}
// 边界判断
if(targetNum > max) {
return max;
} else if( targetNum < min) {
return min;
}
return targetNum;
};