算法通关村——查询元素专题(数组)

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

这是剑指offer中的一道题目,数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。不存在则输出0.

   示例 1:
    输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
    输出: 2

对于没思路的问题,我们通常都是先在脑子中快速过一遍常见的数据结构和常见的算法策略
首先。用排序行不行?先对数组进行排序,在一个有序数组中次数超过一半的必定是中位数。OK,没问题,第一种方法就出来了,这种方法的时间复杂度取决于排序算法的时间复杂度,最快的O(nlogn)。由于排序代价比较高,所以我们继续找其他方法。
其次,用Hash行不行?我们先创建一个HashMap的key是元素的值,value已经出现的次数,然后遍历数组来统计所以元素出现的次数。最后遍历Hash,找到出现次数超过一半的数字。OK,第二种方法出来了
代码如下:


    public int moreThanHalfNum1(int[] array) {
        if (array == null) {
            return 0;
        }
        Map<Integer, Integer> res = new HashMap<>();
        int len = array.length;
        for (int j : array) {
            res.put(j, res.getOrDefault(j, 0) + 1);
            if (res.get(j) > len / 2){
                return j;
            }
        }
        return 0;
    }

上述方法的时间复杂度为O(n),但是这是用O(n)的空间复杂度换来的。那是否有空间复杂度为O(1),时间复杂度为O(n)的呢?

第三种方法。摩尔投票
根据数组特点,数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数之和都要多。因此,我们可以在遍历数组的时候设置两个值:一个是数组中的数result,另一个是 出现次数times。当遍历到下一个数字,次数设为1,。这样,当遍历结束后,最后一次设置的result的值可能就是符合要求的指(如果有数字出现次数超过一半,则必为该元素,否则不存在)因此,判断该元素出现次数是否超过一半即可验证应该返回该元素还是返回0。这种思路是对数组进行了两次遍历。复杂度为O(n)
在这里times最小为0,如果等于0,遇到下一个元素就开始+1,。
如 [1,2,1,3,1,4,1]

开始的时候 result=1,times=1
然后result=2,与上一个不一样,times--为0
然后result=1,与上一个不一样,times已经是0了,遇到新元素就加一为1
然后result=3,与上一个不一样,times--为0
然后result=1,与上一个不一样,times已经是0了,遇到新元素就加一为1
然后result=4,与上一个不一样,times--为0
然后result=1,与上一个不一样,times已经是0了,遇到新元素就加一为1

这里存在的数组的长度是奇偶数,并不影响。如偶数,元素恰好等于一半,很明显最后结果是0,只能说明没有超过一半
代码如下:


    public int moreThanHalfNum2(int[] array) {
        int count = 0;
        int candidate = -1;
        for (int num : array) {
            if (count == 0) {
                candidate = num;
            }
            count += (num == candidate) ? 1 : -1;
        }
        return candidate;
    }

数组中只出现一次的数字

LeetCode136:给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

示例 1 :

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

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

输入:nums = [1]
输出:1

第一种方法:
查重嘛,第一反应肯定是Set集合。题目明确说其他元素都是出现两次,我们也可以利用这个操作,当要添加的元素key与集合中已存在的数重复时,不再进行添加操作,而是将集合中的key一起删除,这样整个数组遍历后,集合中就只剩下了那个只出现一次的数字了,
代码如下:

    public int singleNumber(int[] nums) {
        if (nums == null) {
            return -1;
        }
        Set<Integer> hashSet = new HashSet<Integer>();
        for (int num : nums) {
            if (!hashSet.add(num)) {
                hashSet.remove(num);
            }
        }
        if (hashSet.isEmpty()) {
            return -1;
        }
        return hashSet.toArray(new Integer[0])[0];
    }

注意:必须存在那一个只出现一次的数字,否则Set集合长度就为0,最后一行代码运行时就会出错

第二种方法:位运算
异或运算的几个规则是:

0^0 = 0;
0^a = a;
a^a = 0;
a ^ b ^ a = b

0与其他数字异或的结果是那个数字,相等的数字异或得0。要操作的数组中除了某个数字只出现了一次之外,其他数字都出现了两次,所以可以定义一个变量赋初始值为0,用这个变量与数组中每个数字做异或运算,并将这个变量值更新为那个运算结果,直到数组遍历完毕,最后得到的变量的值就是数组中只出现了一次的数字。这种方法只需遍历一次数组即可。
代码如下:

    public int singleNumber(int[] nums) {
        int flag = 0;
        for (int num : nums) {
            flag ^= num;
        }
        return flag;
    }

颜色分类问题(荷兰国旗问题)

经典算法问题。LeetCode75,也称为荷兰国旗问题。
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

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

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

1、基于冒泡排序的双指针(快慢指针)
冒泡排序我们都知道,就是根据大小逐步和后面的比较,慢慢调整到整体有序。这种方法还是稳定的排序方法
我们可以考虑对数组进行两次遍历。在第一次遍历,我们将数组中所有的0交换到数组的头部,这样第二次遍历只需要处理1和2的问题就行了。
代码如下:

    public void sortColors(int[] nums) {
        int n = nums.length;
        int left = 0;
        // 将所有0的数字移到前面
        for (int right = 0; right < n; right++) {
            if (nums[right] == 0) {
                int temp = nums[right];
                nums[right] = nums[left];
                nums[left] = temp;
                left++;
            }
        }

        // 将所有1的数字移到前面
        for (int right = left; right < n; right++) {
            if (nums[right] == 1) {
                int temp = nums[right];
                nums[right] = nums[left];
                nums[left] = temp;
                left++;
            }
        }
    }

提高一点难度。
如果要求只用一次遍历就要解决问题该怎么办?我们隐约感觉到要使用三个指针才行:

  • left指针,表示left左侧的元素都是0
  • right指针,表示right右侧的元素都是2
  • index指针,从头到尾遍历数组,根据nums[index]是0还是2,决定与left交换还是与right交换

index位置上的数字代表着我们要处理的数字。当index为数字1时,我们只需要index++即可。如果是0.,我们放左边,如果是1,我们放右边。如果index=right,则可以停止
图示:
题解步骤
这里的重点是:index位置为2进行交换后为什么只进行right–,而不用index++呢?这是因为我们right位置交换过来的元素可能是0,也可能是1,如果是0自然没问题,但是如果是1则执行index++,就将1调过了,无法处理,所以我们先不动index,在下一次循环的继续判断这个index位置元素是不是0.
那为啥index位置是0的时候执行swap就可以index了呢?这是因为如果index前面位置如果存在,位置都会被swap到right位置上,这里只需要处理0和1的情况就可以了。
代码如下:

 public void sortColors(int[] nums) {
        int right = nums.length - 1;
        int left = 0, index = 0;

        while (index <= right) {
            if (nums[index] < 1) {
                swap(nums, index++, left++);
            } else if (nums[index] > 1) {
                swap(nums, index, right--);
            } else {
                index++;
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

相关题解代码(含各专题)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值