前端菜鸟学算法(1)

387 阅读6分钟

前言

对于一个前端来说,不管是刚入行的小白还是工作了几年的开发者,算法对于我们大部分人都是最薄弱的技能,没有之一。 对于笔者本人来说,最近面试经常遇到算法问题,也经常被虐,成为了进入大厂最大的瓶颈。所以我决心重新学习算法和数据结构,并在此记录和分享。希望前端菜鸟学算法,能成为一个高质量的学习算法系列博客。

虽无算法之才,亦必有驾驭算法之志!

1.字符串

这个题考察字符串的各种逻辑操作,笔者实现如下:

function matchChar(str) {
	let reg = /0+|1+/
	let matchArr = []
	while(str) {
		let mChar = reg.exec(str)[0]
		let rChar = String(mChar[0] ^ 1)
		let targetChar = mChar + rChar.padStart(mChar.length,rChar)
		str.indexOf(targetChar) == 0 && matchArr.push(targetChar)
		str = str.substr(1)
	}
	return matchArr
}

2.数组

  • 2.1 电话号码组合
    这个题考察对数组api的理解和运用,笔者实现如下:
function arrange() {
	let targetArr = []
	let strMap = {
		2: 'abc',
		3: 'def',
		4: 'ghi',
		5: 'jkl',
		6: 'mno',
		7: 'pqrs',
		8: 'tuv',
		9: 'wxyz',
	}
	let args = [].slice.call(arguments)
	if(args.some(num => num < 2 || num > 9)) {
		return console.log('输入不合法')
	}
	args = args.map(num => strMap[num])
	if(args.length < 1) {
		return []
	}
	if(args.length == 1) {
		return args[0].split('')
	}
	targetArr = compose(...args.splice(0,2))
	while(args.length) {
		let arr = args.splice(0,1)
		targetArr = compose(targetArr,arr[0])
	}
	
	function compose(str1,str2) {
		let ouputArr = []
		let arr1 = Array.isArray(str1) ? str1 : str1.split('')
		let arr2 = str2.split('')
		for(let i = 0, len = arr1.length; i < len; i++) {
			for(let j = 0, len = arr2.length; j < len; j++) {
				ouputArr.push(arr1[i] + arr2[j])
			}
		}
		return ouputArr
	}
	
	return targetArr
}
  • 2.2 卡牌分组

这个题考察对数组api和归纳能力,笔者实现如下:

function cardGroup(arr) {
	if(!Array.isArray(arr)) return false
	arr.sort()
	let newArr = split(arr)
	let lenArr = newArr.map(item => item.length)
	if(lenArr.includes(1)) {
	    return false
	}
	let minNum = Math.min.apply(null, lenArr)
	return lenArr.every(item => item % minNum === 0)
}

function split(arr) {
	let retArr = []
	let tempArr = []
	for(var i = 0; i < arr.length; i++) {
		tempArr.push(arr[i])
		if(!arr[i+1]) {
			retArr.push(tempArr)
			tempArr = []
			break
		}
		if(arr[i] < arr[i+1]) {
			retArr.push(tempArr)
			tempArr = []
		}
	}
	return retArr
}
  • 2.3 种花问题

这个题考察抽象和数学建模的能力,笔者实现如下:

function plantFlowers(arr,fNum) {
	for(var i = 0; i < arr.length; i++) {
		if(arr[i] == 0 && fNum !== 0) {
			if(arr[i-1] !== undefined && arr[i+1] !== undefined) {
				if(arr[i-1] === 0 && arr[i+1] === 0) {
					arr[i] = 1 // 种花
					fNum--
				}
			}else if(arr[i-1] === undefined) { //开头是0的情况
				if(arr[i+1] === 0) {
					arr[i] = 1 // 种花
					fNum--
				}
			}else if(arr[i+1] === undefined) { //结尾是0的情况
				if(arr[i-1] === 0) {
					arr[i] = 1 // 种花
					fNum--
				}
			}
		}
	}
	return {
		ability: fNum === 0,
		arr
	}
}
  • 2.4 格雷编码

这个题考察动态输入和归纳的能力,笔者实现如下:

function genCode(n) {
	
	function grayCode(n) {
		let retArr = []
		if(n==0) return ['0']
		if(n==1) return ['0','1']
		let arr = grayCode(n-1)
		let len = 2**n - 1
		for(var i = 0; i < arr.length; i++) {
			retArr[i] = `0${arr[i]}`
			retArr[len - i] = `1${arr[i]}`
		}
		return retArr
	}
	
	return grayCode(n).map(num => parseInt(num,2))
}

3.正则

  • 3.1 子字符串模式匹配

这个题考察正则基础知识,笔者实现如下:

function match(str) {
	let reg = /^(\w+)\1+$/
	return reg.test(str)
}
  • 3.2 正则表达式匹配

这个题考察正则进阶知识(先行断言和负向先行断言),笔者实现如下:

function match(str, mode) {
	let modeArr = mode.match(/([a-z.]\*)|([a-z]+(?=([a-z.]\*)|$|\.))|(\.(?!\*))/g)
	let cur = 0
	let strLen = str.length
	for(let i = 0, len = modeArr.length, m; i < len; i++) {
		//对于模式分为四类  .*|a*|cdef|非.*组合的单个.
		m = modeArr[i].split('')
		// 如果第二位是*表示是有模式的
		if(m.length == 1 && m[0] == '.') {
			cur ++
		}else if(m[1] === '*') {
			if(m[0] === '.') {
				cur =  strLen
				break
			}else {
				while(str[cur] === m[0]) {
					cur ++
				}
			}
		}else {
			for(let j = 0, jl = m.length; j < jl; j++) {
				if(m[j] !== str[cur]) {
					return false
				}else {
					cur ++ 
				}
			}
		}
	}
	return cur === strLen
}

4.排序

时间复杂度:反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。空间复杂度:指运行完一个程序所需内存的大小

  • 4.1 冒泡排序

function bubbleSort(arr) {
    for(var i = 0 ;i<arr.length-1;i++) { // 遍历数组,次数就是arr.length - 1
	let isOver = true // 加一个标志位
        for(var j = 0;j<arr.length-i-1;j++) { // 当第一次,找到最大数,放到最后,那么下一次,遍历的时候就要不需要比较最大的数了;这里要根据外层for循环的 i,逐渐减少内层 for循环的次数
	    // 如果前一个数 大于 后一个数 就交换两数位置
            if(arr[j]>arr[j+1]) {
                [arr[j],arr[j+1]] = [arr[j+1],arr[j]]
		isOver = false
            }
        } 
        if(isOver) break //如果某次循环完后,没有任何两数进行交换,就将标志位 设置为 true,减少不必要的排序
     }
     return arr 
}       

总结

  • 1、外层 for 循环控制循环次数
  • 2、内层 for 循环进行两数交换,找每次的最大数,排到最后
  • 3、设置一个标志位,减少不必要的循环

时间复杂度:O(n) ~ O(n2) 最好的情况是传入的数组本身就已经排序了,只会走内层循环,加一次外层循环; 空间复杂度: O(1)

  • 4.2 选择排序

function selectionSort(array) {
  for (let i = 0; i < array.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < array.length; j++) {
      if (array[j] < array[minIndex]) {
        minIndex = j;
      }
    }
    [array[minIndex], array[i]] = [array[i], array[minIndex]];
  }
  return array
}

总结

每次循环选取一个最小的数字放到前面的有序序列中

时间复杂度:O(n2) 空间复杂度: O(1)

  • 4.3 插入排序
function insertSort(array) {
    for (let i = 1; i < array.length; i++) {
        let target = i;
        for (let j = i - 1; j >= 0; j--) {
        if (array[target] < array[j]) {
            [array[target], array[j]] = [array[j], array[target]]
            target = j;
        } else {
            break;
        }
        }
    }
    return array;
}

总结

将左侧序列看成一个有序序列,每次将一个数字插入该有序序列。

插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。

时间复杂度:O(n2) 空间复杂度: O(1)

  • 4.4 快速排序
function quickSort(arr){
    if(arr.length<=1) return arr 
    let left=[]
    let right=[]
    let middleInx=Math.floor(arr.length/2)
    let middle=arr.splice(middleInx,1)[0]
    for(var i=0,len=arr.length;i<len;i++){
        if(arr[i]<middle){
            left.push(arr[i])
        }else{
            right.push(arr[i])
        }
    }
    return quickSort(left).concat([middle],quickSort(right))
}

总结

选择一个基准元素target(一般选择第一个数) 将比target小的元素移动到数组左边,比target大的元素移动到数组右边 分别对target左侧和右侧的元素进行快速排序

时间复杂度:平均O(nlogn),最坏O(n2) 空间复杂度:O(logn)

  • 4.5. 计算最大区间
给定一个无序的数组,找出数组在排序之后,相邻元素之间的最大差值。如果数组元素个数小于2,则返回0
例如:
	输入[3,6,9,1] 输出 3

这个题结合了冒泡排序,实现了较高的性能,笔者实现如下:

function calcMax(arr) {
    if(arr.length < 2) return 0 
    let max = 0
	for(var i = 0, len = arr.length - 1; i <= len; i++) {
        for(var j = 0; j < arr.length-i-1; j++) {
            if(arr[j] > arr[j+1]) {
                [arr[j],arr[j+1]] = [arr[j+1],arr[j]]
            }
        }
        if(i) {
          let sub = arr[len+1-i] - arr[len-i]
          if(max < sub) max = sub 
        }
    }
    return max 
}
  • 4.6. 奇偶排序
奇书偶数排序

例如:
	输入[1,3,2,6,4,9] 输出 [2, 1, 4, 3, 6, 9]

排序的一种思路,根据下标的移动来实现:

function crossSort(arr) {
    arr = arr.sort()
    let odd = 1, even = 0, ret = []
    arr.forEach(item=> {
        if(item %2 ==0) {
            ret[even] = item 
            even += 2
        }else {
            ret[odd] = item 
            odd += 2
        }
    })
    return ret 
}
  • 4.7. 缺失的一个正数
给定一个未排序的整数数组,找出其中没有出现的最小的正整数

例子:
	输入:[1,2,0]  输出:3
	输入:[3,4,-1,1] 输出:2
	输入:[7,8,9,11,12] 输出:1

代码实现:

function find(arr) {
    let max = Math.max.apply(null,arr)
    let ret = null
    for(var i = 1; i < max; i++) {
        if(!arr.includes(i)) {
            ret = i 
            break
        }
    }
    if(ret === null) {
	  ret = (max < 0 ? 0 : max) + 1
    }
    return ret 
}

5.递归

复原IP地址

给定一个只包含数字的字符串,复原它并返回所有可能的IP地址格式

例子
	输入 '25525511135' 输出 ['255.255.11.135','255.255.111.35']

代码实现:

function recoverIp(ip) {
	let ret = []
	search(ip,[])
	function search(str,arr) {
		if(arr.length === 4 && arr.join('') === ip) {
			ret.push(arr.join('.'))
		}else {
			for(let i = 0, len = Math.min(3, str.length); i< len; i++) {
				let temp = str.substr(0, i + 1)
				if(temp < 256) {
					search(str.substr(i+1), arr.concat([temp]))
				}
			}
		}
	}
	return ret 
}