算法题总结

201 阅读11分钟

算法题总结

1反转字符串中的单词III

给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

示例:

输入:"Let's take LeetCode contest" 输出:"s'teL ekat edoCteeL tsetnoc"

提示:

在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格。

//第一种解法
export default (str) => {
	return str.split(' ').map(item => {
		return item.split('').reverse().join('')
	}).join(' ')
}
//第二种解法,把上面的split(' ')换为正则表达式
export default (str) => {
	return str.split(/\s/g).map(item => {
		return item.split('').reverse().join('')
	}).join(' ')
}
//第三种解法
export default (str) => {
	return str.match(/[\w']+/g).map(item => {
		return item.split('').reverse().join('')
	}).join(' ')
}

2计数二进制子串

给定一个字符串 s,计算具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是连续的。

重复出现的子串要计算它们出现的次数。

示例 1 :

输入: "00110011" 输出: 6 解释: 有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。

请注意,一些重复出现的子串要计算它们出现的次数。

另外,“00110011”不是有效的子串,因为所有的0(和1)没有组合在一起。 示例 2 :

输入: "10101" 输出: 4 解释: 有4个子串:“10”,“01”,“10”,“01”,它们具有相同数量的连续1和0。

提示:

s.length 在1到50,000之间。 s 只包含“0”或“1”字符。

var countBinarySubstrings = function (s) {
	let all = 0;
	//匹配字符串,拿出连续的0和1的字段,返回数组
	let m = s.match(/([1]+)|(0)+/g);
	if (m.length > 1) {
		//循环,相邻比较
		for (let i = 0; i < m.length - 1; i++) {
			all += Math.min(m[i].length, m[i+1].length);
		}
	} 
	return all;
};

3电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

3电话号码的字母组合.jpg

示例 1:

输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"] 示例 2:

输入:digits = "" 输出:[] 示例 3:

输入:digits = "2" 输出:["a","b","c"]

提示:

0 <= digits.length <= 4 digits[i] 是范围 ['2', '9'] 的一个数字。

export default (str) => {
	//建立电话号码键盘映射
	let map = ['', '', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'];
	//把输入字符串按单字符串分隔变成数组,234 => [2, 3, 4]
	let num = str.split('');
	//保存键盘映射后的字母内容,如23 => ['abc', 'def']
	let code = [];
	num.forEach(item => {
		if (map[item]) {
			code.push(map[item]);
		}
	});
	
	let comb = (arr) => {
		//临时变量用来保存前两个组合的结果
		let tmp = [];
		//最外层的循环是遍历第一个元素,里层的循环是遍历第二个元素
		for (let i = 0, il = arr[0].length; i < il; i++) {
			for (let j = 0, jl = arr[1].length; j < jl; j++) {
				tmp.push(`${arr[0][i]}${arr[1][j]}`);
			}
		}
		//把arr的前两个用循环后的临时变量tmp取代
		arr.splice(0, 2, tmp);
		if (arr.length > 1) {
			//递归
			comb(arr);
		}else {
			return arr[0];
		}
		//视频里这里是这么写的,我感觉应该像上面一样写更简化
		//if (arr.length > 1) {
			////递归
			//comb(arr);
		//}else {
			//return tmp;
		//}
		//return arr[0];
		//截止到这,以上是视频的
	}
	return comb(code);
}

解题思路如图:

3电话号码的字母组合2.jpg

4卡牌分组

给定一副牌,每张牌上都写着一个整数。

此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:

每组都有 X 张牌。 组内所有的牌上都写着相同的整数。 仅当你可选的 X >= 2 时返回 true。

示例 1:

输入:[1,2,3,4,4,3,2,1] 输出:true 解释:可行的分组是 [1,1],[2,2],[3,3],[4,4] 示例 2:

输入:[1,1,1,2,2,2,3,3] 输出:false 解释:没有满足要求的分组。 示例 3:

输入:[1] 输出:false 解释:没有满足要求的分组。 示例 4:

输入:[1,1] 输出:true 解释:可行的分组是 [1,1] 示例 5:

输入:[1,1,2,2,2,2] 输出:true 解释:可行的分组是 [1,1],[2,2],[2,2]

提示:

1 <= deck.length <= 10000 0 <= deck[i] < 10000

//视频里的解析答案
export default (arr) => {
	//对这副牌进行排序,升序、降序都可以
	arr.sort((a, b) => a-b);
	let min = Number.MAX_SAFE_INTEGER;
	let dst = [];
	let result = true;
	for (let i = 0, len = arr.length, tmp = []; i < len; i++) {
		tmp.push(arr[i]);
		for (let j = i + 1; j < len - 1; j++) {
			if (arr[i] === arr[j]) {
				tmp.push(arr[j]);
			}else {
				if (min > tmp.length) {
					min = tmp.length;
				}
				//这里一定要这么写,因为数组是引用类型,
				//直接dst.push(tmp)的话,最后的结果都会被最终的tmp取代
				dst.push([].concat(tmp));
				//临时数组进行清空
				//JS高程里有写,对数组的清空应该用数组名.length = 0来清空,
				//不应该是数组名 = []
				tmp.length = 0;
				//下面这句也很关键
				i = j;
				break;
			}
		}
	}
	//注意,下面为什么用every不用forEach,
	//因为forEach不支持跳出,不支持循环的return、break功能
	dst.every(item => {
		if (item.length % min != 0) {
			result = false;
			return false;
		}
	})
	return result;
}

//我想到的解析答案
export default (arr) => {
	//对这副牌进行排序,升序、降序都可以
	arr.sort((a, b) => a-b);
	let min = Number.MAX_SAFE_INTEGER;
	let result = true;
	//数组先转换为字符串,再匹配字符串,拿出连续的字段,返回数组
	let dst = arr.join('').match(/([0]+)|(1)+|(2)+|(3)+|(4)+|(5)+|(6)+|(7)+|(8)+|(9)+/g);
	//寻找最小组
	for (i = 0, len = dst.length; i < len; i++) {
		if (dst[i].length < min) {
			min = dst.length;
	}
	dst.every(item => {
		if (item.length % min != 0) {
			result = false;
			return false;
		}
	})
	return result;
}

5种花问题

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。

示例 1:

输入:flowerbed = [1,0,0,0,1], n = 1 输出:true 示例 2:

输入:flowerbed = [1,0,0,0,1], n = 2 输出:false

提示:

1 <= flowerbed.length <= 2 * 104 flowerbed[i] 为 0 或 1 flowerbed 中不存在相邻的两朵花 0 <= n <= flowerbed.length

export default (arr, n) => {
	//计数器
	let max = 0;
	for (let i = 0, len = arr.length - 1; i < len; i++) {
		if (arr[i] ===0) {
			if (i === 0 && arr[1] ===0) {
				max++;
				i++;
			}else if (arr[i - 1] === 0 && arr[i + 1] === 0) {
				max++;
				i++;
			}
		}
	}
	return max >= n;
}

6格雷编码

格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异。

给定一个代表编码总位数的非负整数 n,打印其格雷编码序列。即使有多个不同答案,你也只需要返回其中一种。

格雷编码序列必须以 0 开头。

示例 1:

输入: 2

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

解释:

00 - 0

01 - 1

11 - 3

10 - 2

对于给定的 n,其格雷编码序列并不唯一。 例如,[0,2,3,1] 也是一个有效的格雷编码序列。

00 - 0

10 - 2

11 - 3

01 - 1

示例 2:

输入: 0

输出: [0]

解释: 我们定义格雷编码序列必须以 0 开头。 给定编码总位数为 n 的格雷编码序列,其长度为 2n。当 n = 0 时,长度为 2的0次方 = 1。 因此,当 n = 0 时,其格雷编码序列为 [0]。

export default (n) => {
	//递归函数,用来算输入为n的格雷编码序列
	let make = (n) => {
		if (n == 1) {
			return ['0', '1'];
		}else {
			let prev = make(n - 1);
			let result = [];
			let max = Max.pow(2, n) -1;
			for (let i = 0, len = pre.length; i < len; i++) {
				result[i] = `0${pre[i]}`;
				result[max - i] = `1${pre[i]}`;
			}
			return result;
		}
	}
	return make(n);
}

解析图例

6格雷编码.jpg

第一行为0和1并且按照中间区分,接下来的行按照中间对称,依次递归

7重复的子字符串

给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成,给定的字符串只含有小写英文字母,并且长度不超过10000。

示例1:

输入:"abab"

输出:True

解释:可由子字符串"ab"重复两次构成。

示例2:

输入:"aba"

输出:False

示例3:

输入:"abcabcabc"

输出:True

export default (str) => {
	var reg = /^(\w+)\1+$/;
	return reg.test(str);
}

8正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符

'*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

示例 1:

输入:s = "aa" p = "a" 输出:false 解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:s = "aa" p = "a*" 输出:true 解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

输入:s = "ab" p = "." 输出:true 解释:"." 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

输入:s = "aab" p = "cab" 输出:true 解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

示例 5:

输入:s = "mississippi" p = "misisp*." 输出:false

提示:

0 <= s.length <= 20

0 <= p.length <= 30

s 可能为空,且只包含从 a-z 的小写字母。

p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

保证每次出现字符 * 时,前面都匹配到有效的字符

export default (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; i < len; i++) {
		//对于模式分为三类 .*|a*|cdef
		m = modeArr[i].split('');
		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;
}

8.jpg

9冒泡排序

9.jpg

时间复杂度:运行的次数;

空间复杂度:占用的内存

export default (arr) => {
	//冒泡排序
	//循环次数
	for (let i = arr.length - 1; i > 0; i++) {
		//每次循环从最开始到结束
		for (let j = 0; j < i; j++) {
			tmp = arr[j];
			if (tmp > arr[j + 1]) {
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
	return arr;
}

10选择排序

function selectionSort (arr) {
	//选择排序
	var len = arr.length;
	var minIndex, temp;
	for (var i = 0; i < len - 1; i++) {
		minIndex = i;
		for (var j = i + 1; j < len; j++) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j;
			}
		}
		temp = arr[i];
		arr[i] = arr[minIndex];
		arr[minIndex] = temp;
	}
	return arr;
}

[十大经典排序算法(动图演示) - 一像素 - 博客园 (cnblogs.com)](十大经典排序算法(动图演示) - 一像素 - 博客园 (cnblogs.com))

10补--快速排序

从数组中选择一个元素作为基准点; 排序数组,所有比基准值小的元素摆放在左边,而大于基准值的摆放在右边。每次分割结束以后基准值会插入到中间去; 最后利用递归,将摆放在左边的数组和右边的数组在进行一次上述的1和2操作。

var quickSort = function (arr) {
	if (arr.length <= 1) {
		return arr;
	}
	var pivotIndex = Math.floor(arr.length / 2);
	var pivot = arr.splice(privotIndex, 1)[0];
	var left = [];
	var right = [];
	for (var i = 0; i < arr.length; i++) {
		if (arr[i] < piovt) {
			left.push(arr[i]);
		}else {
			right.push(arr[i]);
		}
	}
	return quickSort(left).concat([privot], quickSort(right));
}

11最大间距

给定一个无序的数组,找出数组在排序之后,相邻元素之间最大的差值。

如果数组元素个数小于 2,则返回 0。

示例 1:

输入: [3,6,9,1] 输出: 3 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。

示例 2:

输入: [10] 输出: 0 解释: 数组元素个数小于 2,因此返回 0。

说明:

你可以假设数组中所有元素都是非负整数,且数值在 32 位有符号整数范围内。

请尝试在线性时间复杂度和空间复杂度的条件下解决此问题。

//第一种方法,常规做法export default (arr) => {	//如果数组长度小于2返回0	if (arr.length < 2) {		return 0;	}	//排序	arr.sort();	//用它来保存相邻元素的最大差值	let max = 0;	for (let i = 0, len = arr.length, tmp; i < len; i++) {		tmp = arr[i + 1] - arr[i];		if (tmp < max) {			max = tmp;		}	}	return max;}//第二种方法,利用冒泡排序export default (arr) => {	if (arr.length < 2) {		return 0;	}	let max = 0;	let len = arr.length - 1;	let space;	//遍历次数这里注意,i > 0,对应最后返回那里,特别重要,否则会漏解	for (let i = len, tmp; i > 0; i--) {		for (let j = 0; j < i; j) {			tmp = arr[j];			if (tmp < arr[j + 1]) {				arr[j] = arr[j + 1];				arr[j + 1] = tmp;			}		}		//第二轮后才会把最后两个进行差值比较,但是这种情况只能到arr[2] - arr[1],		//对arr[1] - arr[0]的值取不到,因为上面冒泡排序的外层遍历i > 0,		//所以要在结尾return处进行处理一下		if (i < len) {			space = arr[i + 1] - arr[i];			if (space > max) {				max = space;			}		}	}	//要在这里处理一下arr[1] - arr[0]这种情况,特别重要	return Math.max(max, arr[1] - arr[0]);}

12按奇偶排序数组II

给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数。

对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数;当 A[i] 为偶数时, i 也是偶数。

你可以返回任何满足上述条件的数组作为答案。

示例:

输入:[4,2,5,7] 输出:[4,5,2,7] 解释:[4,7,2,5],[2,5,4,7],[2,7,4,5] 也会被接受。

提示:

2 <= A.length <= 20000

A.length % 2 == 0

0 <= A[i] <= 1000

export default (arr) => {	//进行升序排序	//当然,如果只是排序,完全可以把(a, b) => a - b去掉,直接排序就行	arr.sort((a, b) => a - b);	//声明一个空数组用来存储奇偶排序后的数组	let r = [];	//记录奇数、偶数下标	let odd = 1;	let even = 0;	//对数组进行遍历	arr.forEach(item => {		if (item % 2 === 1) {			r[odd] = item;			odd += 2;		}else {			r[even] = item;			even += 2;		}	})	return r;}

13数组中的第K个最大元素

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2 输出: 5 示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4 输出: 4 说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

//第一种解法,常规解法export default (arr, k) => {	return arr.sort((a, b) => b - a)[k - 1];}//第二种解法,利用冒泡排序export default (arr, k) => {	let len = arr.length - 1;	for (let i = len, tmp; i > len - k; i--) {		for (let j = 0; j < i; j++) {			if (arr[j] > arr[j + 1]) {				tmp = arr[j];				arr[j] = arr[j + 1];				arr[j + 1] = tmp;			}		}	}	return arr[len - (k - 1)];}

14缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

进阶:你可以实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案吗?

示例 1:

输入:nums = [1,2,0] 输出:3 示例 2:

输入:nums = [3,4,-1,1] 输出:2 示例 3:

输入:nums = [7,8,9,11,12] 输出:1 提示:

0 <= nums.length <= 300 -231 <= nums[i] <= 231 - 1

//第一种方法,常规想法export default (arr) => {	//过滤掉非正整数	arr = arr.filter(item => item > 0);	//正整数数组是不是为空	if (arr.length) {		//升序,目的:方便从左到右取最小值arr[0]		arr.sort((a, b) => a - b);		//如果第一个元素不为1,返回1		if (arr[0] !== 1) {			return 1;		}else {			//从左边开始遍历,只要下一个元素和当前元素差值大于1,则就找到了			for (let i = 0, len = arr.length - 1; i < len; i++) {				if (arr[i + 1] - arr[i] > 1) {					return arr[i] + 1;				}			}			//如果数组是连续的从1开始的正整数,则最后一个数+1就是所求的值			return arr.pop() + 1;		}	}else {		//如果是个空数组,直接返回1		return 1;	}}//第二种方法,选择排序export default (arr) => {	arr = arr.filter(item => item > 0);	if (arr.length) {		//实现选择排序,先拿到最小值,如果第一个元素不是1直接返回1,如果是1,就要比相邻元素差值        for (let i = 0, len = arr.length, min; i < len; i++) {            min = arr[i];            for (let j = i + 1; j < len; j++) {                if (arr[j] < min) {                    let c = min;                    min = arr[j];                    arr[j] = c;                }            }            arr[i] = min;            //i > 0 表示遍历至少两次了            if (i > 0) {                if (arr[i] - arr[i - 1] > 1) {                    return arr[i - 1] + 1;                }else {                	return arr.pop() + 1;                }            }else {                if (min !== 1) {                    return 1;                }            }        }	}else {		return 1;	}}

15复原IP地址

给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。

示例 1:

输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"] 示例 2:

输入:s = "0000" 输出:["0.0.0.0"] 示例 3:

输入:s = "1111" 输出:["1.1.1.1"] 示例 4:

输入:s = "010010" 输出:["0.10.0.10","0.100.1.0"] 示例 5:

输入:s = "101023" 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

提示:

0 <= s.length <= 3000 s 仅由数字组成

//第一种方法(B站的视频)export default (s) => {	var ret = [];	//特殊情况	//s的长度小于44*1)或者大于124*3),那么直接返回空数组	if (s.length < 4 || s.length > 12) {		return ret;	}	//这个函数用来判断传入的string转换为数字是否符合ip的(0255)的要求	var isValid = (s) => {		//单个整数都可以,0也可以,但两位数04就不可以了		if (s.length === 1) {			return true;		}else {			//088这样的字符串是不符合要求的			if (s[0] === '0') {				return false;			}			//也可以是if (+s < = 255)			//因为字符型字符串,前面加上一个+,就会将这个字符串转换为数字			if (Number(s) <= 255) {				return true;			}		}	}	//定义一个深度探索的函数	//s是原字符串,P是下标指针,str是已经组成的字符串,cnt是已经存在的几组数字	var def = (s, p, str, cnt) => {		if (cnt == 4 && p == s.length) {			ret.push(str);			return;		}		//cnt = 4,但是p却没走到末尾,p走到末尾但是cnt却没达到4,这两种都不行,直接放弃		if (cnt == 4 || p == s.length) {			return;		}		//i可以取123		for (let i = 1; i < 4; i++) {			//如果指针已经走超过了末尾,直接跳出循环			if (p + i > s.length) {				break;			}			let cutStr = s.slice(p, p + i);			if (isValid(cutStr)) {				//如果指针没到末尾则不需要加上'.'				dfs(s, p + i, str + (p + i == s.length ? curStr : curStr + '.'), cnt + 1);			}else {				continue;			}		}	}	dfs(s, 0, '', 0);	return ret;}

16串联所有单词的子串

给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。

示例 1:

输入: s = "barfoothefoobarman", words = ["foo","bar"] 输出:[0,9] 解释: 从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。 输出的顺序不重要, [9,0] 也是有效答案。 示例 2:

输入: s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"] 输出:[]

export default (s, words) {	if (!words || !words.length) return [];	let wordLen = words[0].length;	let allWordsLen = wordLen * words.length;	let ans = [], wordMap = {};	//遍历数组,改为Map,如果不存在则为1,如果存在则加1	for (let w of words) {		wordMap[w] ? wordMap[w]++ : wordMap[w] = 1;	}	for (let i = 0; i < s.length - allWordsLen + 1; i++) {		//把wordMap复制给wn		let wn = Object.assign({}, wordMap);		//遍历查验Map,		//i + allWordsLen - wordLen +1表示从i开始到整个数组.length前一个wordLen的后一位		for (let j = i; j < i + allWordsLen - wordLen +1; j += wordLen) {			let w = s.slice(j, j + wordLen);			if (wm[w]) {				wm[w]--;			}else {				break;			}		}		if (Object.values(wm).every(n => n === 0)) ans.push(i);	}	return ans;}