每日算法题4 —— 次数超过一半的数字,只出现一次的两个数字,某个元素仅出现一次而其余元素都恰出现 N次

127 阅读4分钟

数组中出现次数超过一半的数字

数组中出现次数超过一半的数字 - 数组中出现次数超过一半的数字 - 力扣(LeetCode)

方法一:哈希表统计法

思路:遍历数组,用map统计各个数字出现的次数

代码实现

var majorityElement = function(nums) {
    let map = new Map()
    for(let i =0; i< nums.length;i++) {
        if(map.has(nums[i])) {
            map.set(nums[i], map.get(nums[i]) + 1)
        }else {
            map.set(nums[i], 1)
        }
    }
    for(let [key, value] of map.entries() ) {
        if(value > nums.length / 2) {
            return key
        }
    }
};
  • 时间复杂度:O(n)其中 n是字符串 的长度
  • 空间复杂度:  O(n),哈希表最多包含n−[n/2]个键值对,所以占用的空间为O(n)。

方法二:排序

思路:题目说数组中有一个数字出现的次数超过数组长度一半,若对数组进行排序后, 那么数组的中间位置必然就是那个数字。

var majorityElement = function(nums) {
    nums.sort((a,b) => a-b)
    return nums[nums.length >> 1]
};
  • 时间复杂度:O(nlogn),数组排序的时间复杂度为O(nlogn)
  • 空间复杂度:  O(logn),如果使用语言自带的排序算法,需要使用O(logn)栈空间,如果自己编写堆排序,则只需要使用O(1)的额外空间。

方法三:摩尔投票法

摩尔投票法:核心理念为 票数正负抵消。此方法时间和空间复杂度分别为O(n)和 O(1),为最佳解法。

下面代码虽然通过了,但有些问题,若数组为[2,2,2,1,3,5,4,2]会出错返回4,目前还没想到解决方法!有小伙伴知道的可以在评论区留言啊!感谢!!!

var majorityElement = function(nums) {
    let count = 0  //票数
    let many 
    for(let i = 0; i < nums.length; i++) {
        if(count ==0) {
            many = nums[i]
        }
        count += (nums[i] == many ? 1 : -1)
    }
    return many
};

数组中只出现一次的两个数字

剑指 Offer 56 - I. 数组中数字出现的次数 - 力扣(LeetCode)

题目:一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

方法一:分组异或

思路与算法:假设数组 nums 中只出现一次的元素分别为 x1 和 x2。如果把 nums中的所有元素全部异或起来,得到结果 x,那么一定有 x = x1 ^ x2

且 x显然不会等于 0,因为如果 x=0,那么说明 x1 = x2,这样 x1 和 x2就不是只出现一次的数字了。因此可以使用位运算 x & -x   或者  x & (~x + 1)    取出 x 的二进制表示中最低位的那个1,设其为第 l 位,那么 x1 和 x2中的某一个数的二进制表示的第 l 位为0,另一个数的二进制表示的第 l 位为1。在这种情况下,x1^x2的二进制表示的第 l位才能为1.

这样一来,就可以把 nums中的所有元素分为两类,其中一类包含所有二进制表示的第 l 位为0 的数,另一类包含所有二进制表示的第 l 位为1的数。可以发现:

  • 对于任意一组在数组nums中出现两次的元素,该元素的两次出现会被包含在同一类中;
  • 对于任意一个在数组nums中只出现一次的元素,即 x1和 x2,它们会被包含在不同类中。

因此,如果将每一类元素全部异或起来,那么其中一类会得到 x1,另一类会得到x2。这样就找出了这两个只出现一次的元素。

var singleNumber = function(nums) {
    let xor = 0
    for(let num of nums) {
        xor ^= num
    }
    let bin = xor & (-xor)
    let type1 = 0, type2 = 0
    for(let num of nums) {
        if(num & bin) {
            type1 ^= num
        } else {
            type2 ^= num
        }
    }
    return [type1, type2]
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

方法二:哈希表,但空间复杂度超出要求

思路:可以使用一个哈希映射统计数组中每一个元素出现的次数。

在统计完成后,对哈希映射进行遍历,将所有只出现了一次的数放入答案中。

var singleNumber = function(nums) {
    let map = new Map()
    for(let num of nums) {
        map.set(num, (map.get(num) || 0) + 1)
    }
    let res = []
    for(let [num, count] of map) {
        if(count == 1) {
            res.push(num)
        }
    }
    return res
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n),即为哈希映射需要使用的空间

某个元素仅出现一次,其余元素都恰出现 N次

剑指 Offer 56 - II. 数组中数字出现的次数 II - 力扣(LeetCode)

思路与算法:

为了方便叙述,称 只出现一次的元素 为 答案

由于数组中的元素都在 int(即32位整数)范围内,因此可以依次计算答案的每一个二进制位是 0 还是 1。

具体地,考虑答案的第 i 个二进制位(i 从0开始编号),它可能为 0 或 1。对于数组中非答案的元素,每个元素出现了 n 次,对应着第 i 个二进制位的 n 个1 或 n个0,无论哪种情况,它们的和都是 n 的倍数(即和为 0 或 n)。因此:

答案的第 i 个二进制位就是数组中所有元素的第 i 个二进制位之和除以 n 的余数。

这样一来,对于数组中的每一个元素x,使用位运算  ( x >> i) & 1得到 x 的第 i个二进制位,并将它们相加再对 n 取余,得到的结果一定为 0 或 1,即为答案的第 i个二进制位。

>>为右移,各二进制位右移若干,对于无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)

<< 左移,各二进制位全左移若干,高位丢弃,低位补0

&为与运算,两个位都为 1 时,结果才为 1

|为或运算,两个位都为 0 时,才为 0

细节:

值得注意的是,如果使用的语言对 [有符号整数类型] 和 [无符号整数类型] 没有区分,那么可能会得到错误的答案。因为 [有符号整数类型] (即int类型)的第31个二进制位(即最高位)是补码意义下的符号位,对应着 -2^(31),而 [无符号整数类型] 由于没有符号,第31个二进制位对应着 2^(31)。因此在某些语言(如python)需要对最高位进行特殊判断。

var singleNumber = function(nums) {
    let res = 0
    for(let i = 0; i < 32; i++) {
        let total = 0
        for(let num of nums) {
            total += (num >> i) & 1
        }
        if(total % 3 !== 0) {
            res |= (1 << i)
        }
    }
    return res
};
  • 时间复杂度:O(nlogC),且其中n为数组的长度,C是元素的数据范围,在本题中log C = log 2^32 = 32,也就是需要遍历第0 ~31个二进制位
  • 空间复杂度:O(1)

方法二:哈希表

思路与算法:用哈希表映射统计数组中每个元素的出现次数。对于哈希映射中的每个键值对,键表示一个元素,值表示其出现的次数。

在统计完成后,遍历哈希映射即可找出只出现一次的元素。

var singleNumber = function(nums) {
    let map = new Map()
    nums.forEach(item => {
        map.set(item, (map.get(item) || 0) + 1)
    })
    let res = 0
    for(let [num, count] of map.entries()) {
       if( count == 1) {
           res = num
           break   
       }
    }
    return res
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n),哈希映射中包含最多 [total / n] + 1个元素,即需要的空间为O(n)

某个元素仅出现一次,其余元素都恰出现2次的另外解法

136. 只出现一次的数字 - 力扣(LeetCode)

方法:位运算

function singleNumber(nums) {
    let res = 0
    for(let i of nums) {
        res ^= i
    }
    return res
}

异或运算(^),在两个二进制位不同时返回1,相同时返回0,联想消消乐。

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

连续子数组的最大和

连续子数组的最大和 - 连续子数组的最大和 - 力扣(LeetCode)

思路:记录一个当前连续子数组最大值 max 默认值为数组第一项

1.从数组第二个数开始,若 sum<0 则当前的sum不再对后面的累加有贡献,sum = 当前数

2.若 sum>0 则sum = sum + 当前数

3.比较 sum 和 max ,max = 两者最大值

var maxSubArray = function(nums) {
    let sum = nums[0]
    let max = nums[0]
    for(let i = 1; i < nums.length; i++) {
        if(sum < 0) {
            sum = nums[i]
        } else {
            sum += nums[i]
        }
        if(sum > max) {
            max = sum
        }
    }
    return max
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

方法二:动态规划

image.png

空间复杂度降低:由于dp[i] 只与dp[i−1] 和nums[i] 有关系,因此可以将原数组nums 用作dp 列表,即直接在nums 上修改即可。由于省去dp 列表使用的额外空间,因此空间复杂度从O(N) 降至O(1) 。

var maxSubArray = function(nums) {
    for(let i = 1; i <nums.length; i++) {
        nums[i] += Math.max(0, nums[i-1])
    }
    return Math.max.apply(null, nums)
};
  • 时间复杂度:O(N),线性遍历数组nums 即可获得结果,使用O(N) 时间。
  • 空间复杂度:O(1),使用常数大小的额外空间。

扑克牌顺子

剑指 Offer 61. 扑克牌中的顺子 - 力扣(LeetCode)

题目:从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

思路面试题61. 扑克牌中的顺子(集合 Set / 排序,清晰图解) - 扑克牌中的顺子 - 力扣(LeetCode)

根据题意,此 5 张牌是顺子的 充分条件 如下:

  1. 除大小王外,所有牌 无重复
  2. 设此 5 张牌中最大的牌为 max ,最小的牌为** min (大小王除外)**,则需满足:max−min<5

因而,可将问题转化为:此 5 张牌是否满足以上两个条件?

方法一:set + 遍历

  • 遍历五张牌,遇到大小王(即0)直接跳过
  • 判别重复: 利用 Set 实现遍历判重
  • 获取最大 / 最小的牌
var isStraight = function(nums) {
    let set = new Set()
    let max = 0, min = 14
    for(let num of nums) {
        //跳过大小王
        if(num == 0) continue
        //有重复直接返回false
        if(set.has(num)) {
            return false
        }
        set.add(num)
        max =Math.max(num, max)
        min = Math.min(num, min)
    }
    return max - min < 5
};
  • 时间复杂度:O(n) = O(5) = O(1)
  • 空间复杂度:O(n) = O(5) = O(1),用于判重的辅助Set使用的额外空间

方法二:排序 + 遍历

  • 先对数组执行排序。
  • 判别重复: 排序数组中的相同元素位置相邻,因此可通过遍历数组,nums[i]=nums[i+1] 是否成立来判重。
  • 获取最大 / 最小的牌: 排序后,数组末位元素nums[4] 为最大牌;元素nums[count] 为最小牌,其中count 为大小王的数量。
var isStraight = function(nums) {
    nums.sort((a,b) => a- b)
    let count = 0
    for(let i =0; i < nums.length - 1; i++) {
        if(nums[i] === 0) {
            count++
        }else if(nums[i] == nums[i+1]) {
            return false
        }

    }
    return nums[4] - nums[count] < 5
};

时间复杂度:O(NlogN)=O(5log5)=O(1),其中nums 长度,本题中N=5 ;数组排序使用O(NlogN) 时间。

空间复杂度:O(1),变量count 使用O(1) 大小的额外空间。