每日一道算法:两数之和

192 阅读5分钟

题目:给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

说明:你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例: 给定 nums = [2, 7, 11, 15],target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]

每日一道算法:两数之和

解法一:暴力法

思路分析:

暴力法很简单,遍历每个元素 x,并查找是否存在一个值与 target−x 相等的目标元素

PHP代码实现:

/**
 * @param Integer[] $nums
 * @param Integer $target
 * @return Integer[]
 */
function twoSum($nums, $target) {
	for($i=0;$i<count($nums);$i++){
		for($j=$i+1;$j<count($nums);$j++){
			if($nums[$i] + $nums[$j] == $target){
				return [$i,$j];
			}
		}
	}
}

复杂度分析:

时间复杂度:O(n^2)
对于每个元素,我们试图通过遍历数组的其余部分来寻找它所对应的目标元素,这将耗费 O(n) 的时间
空间复杂度:O(1)

解法二:两遍哈希表

思路分析:

为了对运行时间复杂度进行优化,我们需要一种更有效的方法来检查数组中是否存在目标元素。如果存在,我们需要找出它的索引。保持数组中的每个元素与其索引相互对应的最好方法是什么?哈希表(hashTable)。

通过以空间换取速度的方式,我们可以将查找时间从 O(n) 降低到 O(1)。哈希表正是为此目的而构建的,它支持以 近似 恒定的时间进行快速查找。我用“近似”来描述,是因为一旦出现冲突,查找用时可能会退化到 O(n)。但只要你仔细地挑选哈希函数,在哈希表中进行查找的用时应当被摊销为 O(1)。

一个简单的实现使用了两次迭代。在第一次迭代中,我们将每个元素的值和它的索引添加到表中。然后,在第二次迭代中,我们将检查每个元素所对应的目标元素(target - nums[i])是否存在于表中。注意,该目标元素不能是 nums[i] 本身!

采用数组函数 `array_keys()` 来解题, 返回包含数组中所有键名的一个新数组
`array_keys()` 是一个哈希函数

PHP代码实现:

/**
 * @param Integer[] $nums
 * @param Integer $target
 * @return Integer[]
 */
function twoSum($nums, $target) {
	for($i=0;$i<count($nums);$i++){
		$difNum = $target - $nums[$i];
		$keys = array_keys($nums,$difNum);
		foreach($keys as $key){
			if($key && $key != $i){
				return [$i,$key];
			}
		}
	}
}

复杂度分析:

时间复杂度:O(n)
我们把包含有 n 个元素的列表遍历两次。由于哈希表将查找时间缩短到 O(1) ,所以时间复杂度为 O(n)
空间复杂度:O(n)
所需的额外空间取决于哈希表中存储的元素数量,该表中存储了 n个元素

解法三:一遍哈希表

思路1:array_keys_exists()

思路分析:

事实证明,我们可以一次完成。在进行迭代并将元素插入到表中的同时,我们还会回过头来检查表中是否已经存在当前元素所对应的目标元素。如果它存在,那我们已经找到了对应解,并立即将其返回。

采用数组函数 `array_key_exists()` 来解题, 判断数组是否存在此键名
`array_key_exists()` 是一个哈希函数

PHP代码实现:

/**
 * @param Integer[] $nums
 * @param Integer $target
 * @return Integer[]
 */
function twoSum($nums, $target) {
	$find = [];
	for($i=0;$i<count($nums);$i++){
		$difNum = $target - $nums[$i];
		if(array_key_exists($difNum,$find)){
			return [$find[$difNum],$i];
		}
		$find[$nums[$i]] = $i;
	}
}

复杂度分析:

时间复杂度:O(n)
我们只遍历了包含有 n个元素的列表一次。在表中进行的每次查找只花费 O(1)的时间
空间复杂度:O(n)
所需的额外空间取决于哈希表中存储的元素数量,该表最多需要存储 n个元素

思路2:使用isset()代替array_key_exists()

思路分析:

`isset()`性能比`array_key_exists()`要好,因为`isset()`是语言结构,而`array_key_exists()`是函数,语言结构的解析运行要比函数快一点。

PHP代码实现:

/**
 * @param Integer[] $nums
 * @param Integer $target
 * @return Integer[]
 */
function twoSum($nums, $target) {
	$find = [];
	for($i=0;$i<count($nums);$i++){
		$difNum = $target - $nums[$i];
		if(isset($find[$difNum])){
			return [$find[$difNum],$i];
		}
		$find[$nums[$i]] = $i;
	}
}

复杂度分析:

时间复杂度:O(n)
空间复杂度:O(n)
小贴士:
* isset()效率高于array_key_exists(), PHP7之后有30%左右的提升, php5.6有将近70%的提升
* isset()是语法结构, array_key_exists()是函数, 调用开销要小
* isset()通过 Z_TYPE_P 获取变量类型,然后再进行判断实现的; array_key_exists()则是通过hash查找来实现的
* 对于数组,isset()的性能要高于array_key_exists() 所以,如果数组比较大,我们应该用如下方法保证性能和准确性 isset()

解法四:二分查找法

思路分析:

用一个排序都能把复杂度降到 O(nlogn),通过排序,然后用两个指针从前后扫描逼近真值。

注意这个思想,可以让 O(n^2) 的复杂度降为 O(n),充分利用排序,因为一定会有一个值满足,然后通过值去原数组里找对应的下标。

前提,该数组已经是一个有序数组,必须先排序,再查找

PHP代码实现:

/**
 * @param Integer[] $nums
 * @param Integer $target
 * @return Integer[]
 */
function twoSum($nums, $target) {
	$origin = $nums;
	sort($nums);
	$left = 0;
	$right = count($nums) - 1;
	while($left <= $right){
		if($nums[$left] + $nums[$right] == $target){
			$start = array_keys($origin,$nums[$left]);
			$end = array_keys($origin,$nums[$right]);
			return [reset($start),end($end)];
		}elseif($nums[$left] + $nums[$right] > $target){
			$right -= 1;
		}elseif($nums[$left] + $nums[$right] < $target){
			$left += 1;
		}
	}
}

复杂度分析:

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

四种解法的性能对比

每日一道算法:两数之和

时间复杂度:

Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<O(n^2)<O(n^3)<…<Ο(2n)<Ο(n!)

结论:

一般算法是否最优,主要看时间复杂度,往往最优的算法需要牺牲空间复杂度
一遍哈希表 < 二分查找法 < 两遍哈希表 < 暴力法

github

LeetCode_PHP:https://github.com/zhangdejian/LeetCode_PHP

后记

自己只能解出第一和第二种思路,原来还有其它思路解,soga,涨知识了。

自己不属于那种最聪明的或最有天赋的人,只能后天靠努力来补啦哈,反正多刷刷算法题,准没错,可以让自己的思维和深度无限延伸。

题目来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/tw…