剑指 Offer II 070 排序数组中只出现一次的数字

给定一个只包含整数的有序数组 nums ,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字。

示例 1:

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

输入: nums =  [3,3,7,7,10,11,11]
输出: 10

排序数组中只出现一次的数字


方法一:直接遍历

从0开始遍历,每次递进2,如果第i个和第i+1个不相等, 那么第i个就是结果。

class Solution {
    func singleNonDuplicate(_ nums: [Int]) -> Int {

        for i in 0 ..< (nums.count/2)  {
            if nums[2*i] == nums[2*i+1] {
                continue
            } else {
                return nums[2*i]
            }
        }
        // 前面的遍历不包含最后一个元素,比如[1,1,2], 如果到这里那结果肯定是最后一个元素
        return nums.last!
    }
}

优点: 空间复杂度为O(1),简单粗暴,比较容易想到.

缺点: 只能针对数组是有序的, 并且需要O(N)的时间复杂度

方法二:字典计数

1.把数组中的值作为key记录到字典中, value为出现的次数, 

2.遍历字典, 找出只出现1次的key

class Solution {
    func singleNonDuplicate(_ nums: [Int]) -> Int {
        var dic = [Int:Int]()
        // 遍历数组, 把数字出现的次数记录到字典中
        for num in nums {
            if let value = dic[num] {
                dic.updateValue(value+1 , forKey: num)
            } else {
                dic.updateValue(1 , forKey: num)
            }
        }
        // 遍历字典,找出出现1次的数字
        for (key,value) in dic {
            if value == 1 {
                return key
            }
        }

        return 0
    }
}

优点是对排序不敏感, 无论是乱序还是排序后的结果, 都能找出来, 适用性强;

缺点是需要额外的一块空间O(N), 而且需要遍历2次, 时间复杂度也是O(N).


方法三:所有数组元素异或的结果就是那个唯一的元素;

异或有一个特点, 任何数和自身异或结果肯定为0. 

那么就可以, 全量把数组中的元素异或一次, 只出现一次的元素就是异或的最终结果.

class Solution {
    func singleNonDuplicate(_ nums: [Int]) -> Int {
        var result = 0
        // 遍历数组, 每个数字都执行异或操作
        for num in nums {
            result ^= num
        }
        return result
    }
}

优点: 对排序不敏感, 无论是乱序还是排序后的结果, 都能找出来, 适用性强; 不需要额外的空间, 空间复杂度为O(1);

缺点: 数组必须要遍历完, 时间复杂度为O(N)

方法四:二分模拟

假设只出现一次的元素位于下标 x,由于其余每个元素都出现两次,因此下标 x 的左边和右边都有偶数个元素,数组的长度必定是奇数。由于数组是有序的,因此数组中相同的元素一定相邻。由于下标 x 是相同元素的开始下标的奇偶性的分界,因此可以使用二分查找的方法寻找下标 x。

  1. 先用二分法找到中间值, 
  2. 检查中间值是不是唯一值
    1. 是唯一值,返回此值
    2. 不是唯一值,确定唯一值中间值的左侧还是右侧, 缩小查找区间(这步比较难)

面向测试编程, 在写代码之前提前准备好测试用例, 有助于分析各种情况,找到思路.

  • 1,1,2,2,3,3,4 (唯一值在最右侧)
  • 1,2,2,3,3,4,4 (唯一值在最左侧)
  • 1 (数组中只有一个元素)
  • 1,1,2,3,3(唯一值恰好是中间值)

在做的过程中,还发现数组的个数会影响唯一值在区间左侧和区间右侧的判断逻辑, 需要在补充测试用例,

比如 1,2,2,3,3 与 1,2,2,3,3,4,4 对比可以发现, 中间值的奇偶会影响取区间范围

  • 第一个的中间为2 与后一个值3, 不相等, 应该取左侧区间
  • 第二个中间值为3 与后一个值3相等, 也应该取左侧区间

补充2个测试用例,

  • 1,1,2,2,3,(唯一值在最右侧)
  • 1,2,2,3,3 (唯一值在最左侧)

如此测试用例的各种case都已覆盖, 在写的过程中需要时刻带入不同的测试用例检验自己的写法

class Solution {
    func singleNonDuplicate(_ nums: [Int]) -> Int {

        var midIndex = 0
        var left = 0
        var right = nums.count - 1
        while left <= right {
            // 找到最中间的值
            midIndex = (left + right) / 2
            let midValue = nums[midIndex]
            // 中间值是不是唯一值
            if isThisNum(nums: nums, index: midIndex) {
                return midValue
            } else {
                // 中间值不是唯一值, 根据不同的情况, 设置left,right, 缩小下次查找范围
                if midIndex % 2 == 0 { // 是偶数
                    // 当前值和后一个值一样,说明唯一值在右侧, 比如[1,1,2,2,3]
                    if nums[midIndex] == nums[midIndex + 1] {
                        left = midIndex
                    } else {
                        // 当前值和后一个值不一样,说明唯一值在左侧, 比如[1,2,2,3,3]
                        right = midIndex - 1
                    }

                } else { // 是奇数
                    // 当前值和后一个值一样,说明在左侧, 比如[1,2,2]
                    if nums[midIndex] == nums[midIndex + 1] {
                        right = midIndex
                    } else {
                        // 当前值和后一个值不一样,说明在左侧, 比如[1,1,2]
                        left = midIndex + 1
                    }
                }
            }
        }
        return nums[midIndex]
    }

    // 判断这个下表对应的值,是不是那个唯一的数字
    func isThisNum(nums: [Int], index: Int) -> Bool {
        if nums.count == 1 {
            return true
        }

        var result = false
        // 此数字为最后一个数字, 只判断与前一个数字的关系
        if index == nums.count - 1 {
            if nums[index] == nums[index - 1] {
                result = false
            } else {
                result = true
            }
        } else if index == 0 {
            // 此数字为第一个数字, 只判断与后一个数字的关系
            if nums[index] == nums[index + 1] {
                result = false
            } else {
                result = true
            }
        } else {
            // 此数字为中间的任意数字, 判断与前后的关系,只要有一个相等,就不是目标值
            if nums[index] == nums[index + 1] {
                result = false
            } else if nums[index] == nums[index - 1] {
                result = false
            } else {
                result = true
            }
        }
        return result
    }
}

优点: 时间复杂度为O(N) ,空间复杂度为O(1)

缺点: 代码逻辑复杂,判断很多.需要考虑各种不同的异常case

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值