有一个数组data[n-1]存储了1~n中的n-1个数,问data中缺少的数字是多少【每日一题】

本文介绍了在一个包含1到n中n-1个数字的数组中,如何找出缺失的数字。基础方法包括排序、额外空间记录和加减法,但存在时间复杂度或空间复杂度问题。高级解决方案利用位运算的异或性质,提供了一个时间复杂度为O(n),空间复杂度为O(c),且无溢出问题的高效算法。

今天参加面试的时候面试官问了这个问题,当时很快速的想出了三种解决方案,当时还自我感觉挺好,回来后跟师兄一说,师兄说这个你没想出最好的方案来。: (

问题描述

有一个数组data,大小是n-1,其中存储的是1~n中的数字,不重复,即1~n中只有一个数字不在该数组内,找出该数字。

基础方法

先把我想出的几种方法例举一下吧。

排序 时间复杂度O(nlogn),空间复杂度O(c)

算法描述

先将data中的元素排序,然后从头开始遍历,如果遍历中发现元素有间断,则为确实的数据。

该算法中,排序的时间复杂度为O(nlogn),然后再遍历一次,因此综合时间复杂度是O(nlogn),空间复杂度O(c)。

代码
#include <stdio.h>
#include <stdlib.h>

int cmp (const void *a, const void *b) { 
    return *(int *)a - *(int *)b; 
}

// 找到不存在的元素
// data: 目标数组
// n: 数组中元素的最大可能值
//    data数组的大小为n-1
int findLost(int data[], int n) {
    // 快排
    qsort(data, n - 1, sizeof(int), cmp);
    int i;
    for (i = 0; i < n; ++i) {
        if (data[i] != i + 1)   // 如果第i个数不等于i+1,则返回i+1
            return i + 1;
    }

    // 程序执行到这里说明前面1~n-1个数都不缺,缺第n个数
    return n;
}

int main(void) {
    int data[8] = {1, 2, 3, 5, 8, 6, 7, 4};
    printf("%d\n", findLost(data, 9));
    return 0;
}

额外空间记录 时间复杂度O(n),空间复杂度O(n)

算法描述

用一个大小为n的数组exists,标记目标数组中的元素是否存在,遍历目标数组,修改exists相应位置的值,最后遍历exists找到不存在的元素。

该算法中需要遍历一次目标数组,遍历两次exists数组(初始化+查看数组值),时间复杂度O(n),空间复杂度O(n)

代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// 找到不存在的元素
// data: 目标数组
// n: 数组中元素的最大可能值
//    data数组的大小为n-1
int findLost(int data[], int n) {
    bool *exists = malloc(sizeof(bool) * n);
    int i;
    // 初始化
    for (i = 0; i < n; ++i)
        *(exists + i) = false;
    // 遍历data,修改标志位
    for (i = 0; i < n - 1; ++i)
        *(exists + data[i] - 1) = true;
    // 遍历exists,找到不存在的元素
    for (i = 0; i < n; ++i)
        if (!*(exists + i)) {
            free(exists);
            return i + 1;
        }
    // 对于有效数据,程序不会执行到这里
    free(exists);
    return 0; 
}

int main(void) {
    int data[8] = {1, 8, 3, 5, 9, 6, 7, 4};
    printf("%d\n", findLost(data, 9));
    return 0;
}

加减法 时间复杂度O(n),空间复杂度O(c)

算法描述

计算1+2+…+n,用该值减去data中的每个数,差就是缺少的数。

该算法简单易懂,且计算1+2+…+n可直接用公式n*(n+1)/2,然后减去data中的每个数需要遍历data,时间复杂度O(n),空间复杂度O(c)。

代码
#include <stdio.h>

// 找到不存在的元素
// data: 目标数组
// n: 数组中元素的最大可能值
//    data数组的大小为n-1
int findLost(int data[], int n) {
    int sum = n * (n + 1) / 2;
    int i;
    for (i = 0; i < n - 1; ++i)
        sum -= data[i];
    return sum;
}

int main(void) {
    int data[8] = {1, 8, 3, 5, 9, 6, 7, 4};
    printf("%d\n", findLost(data, 9));
    return 0;
}

基础方法总结

对于上述三种方法,第一种时间复杂度不好,第二种空间复杂度不好,第三种时间复杂度和空间复杂度都不错,但是有一个隐藏的缺陷。当时面试官问我:

“如果我的n很大呢?”

当时我没明白他的意思,我还以为他时间复杂度,我说,n很大也没事儿啊,一次遍历O(n)。现在想想真的是太天真了。面试官想说的是:

当n很大的时候,n*(n+1)/2是有可能溢出的,这种情况下加减法的这种解决方案是有问题的

那么,该如何解决或避免这个问题呢?或许有人说可以换用范围更大的数据类型,但是这不是面试官最想听的答案,答案请接着往下看。

高级解决方案

首先,这个方案使用的位运算中的异或(^),a^b当a和b不相等时为1,相等时为0。
其次,这个方案是下面这个题的变种(参考http://blog.csdn.net/zhengzhb/article/details/7289325

有2n+1个数,其中有2n个数出现过两次,找出其中只出现一次的数

来看我们的原题,data[n-1]中存了n-1个数,每个数都是1~n之间的数,且没有重复,那么我们如果用这个n-1个数再加上1~n就变成了2n-1个数,且其中只有一个数出现了一次,其他均出现了两次。这就变成了上面这个题了。

算法描述

将data中的所有元素进行异或运算,然后再将结果与1~n每个元素依次异或,最后得到的结果就是缺少的元素(只出现了一次的元素)。

我们来论证一下这个算法的正确性:

  1. 0 ^ 1 = 1, 1 ^ 0 = 1, 0 ^ 0 = 0, 1 ^ 1 = 0

  2. 对于任意整数n,n ^ 0 = n, n ^ n = 0
    (1)当n与0异或时,由于0的所有二进制位均为0,因此,n的二进制位中为1的与0相应位的二进制位0异或结果为1,n的二进制位中为0的与0相应位的二进制位0异或结果为0,因此异或后的结果与n本身完全相同;(2)当n与n异或时,由于其二进制位完全相同,而根据1中0 ^ 0 = 0, 1 ^ 1 = 0,n ^ n结果的所有位均为0,所以结果为0。

  3. 异或运算满足交换结合律 a ^ b ^ c = a ^ c ^ b.
    其实我们可以将所有的abc均看做二进制形式,其结果可以看做是如下运算:
    00000000 00000000 00000000 00000010 a = 2
    ^
    00000000 00000000 00000000 00000001 b = 1
    ^
    00000000 00000000 00000000 00000100 c = 4


    00000000 00000000 00000000 00000111 result = 7
    即所有运算数的每一位分别异或,因此不论运算顺序如何,结果都相同。

结论
综合1、2、3,然后再根据我们的数据的特点,有2n-1个数,其中有n-1个数出现了两次,只有一个数出现了1次,那么我们将所有的2n-1个数进行异或时,可以看成如下过程,对于出现了两次的元素,x ^ x = 0,然后是n-1个0和剩余的那个只出现了一次的y进行异或,n-1个0异或的结果还是0,最后再与y异或结果是y,y就是我们要找的缺失的元素,因此上述算法是正确的。

这个算法,需要将所有元素做异或运算,时间复杂度O(n),空间复杂度O(c),而且不会有溢出的问题,这是面试官最喜欢的答案了。

代码

#include <stdio.h>

// 找到不存在的元素
// data: 目标数组
// n: 数组中元素的最大可能值
//    data数组的大小为n-1
int findLost(int data[], int n) {
    int result = 0;
    int i;
    for (i = 0; i < n - 1; ++i) {
        result ^= data[i];
        result ^= (i + 1);
    }
    result ^= n;
    return result;
}

int main(void) {
    int data[8] = {1, 8, 3, 5, 9, 6, 7, 4};
    printf("%d\n", findLost(data, 9));
    return 0;
}

代码中稍微做了一点点优化,在前n-1次循环的时候执行两次异或,分别异或data[i]和i+1,这样,代码循环只执行了n-1次,比两个for循环少一次循环。

嗯,面试官最喜欢的就是最后这个答案了。悔不早知 : (


// 个人学习记录,若有错误请指正,大神勿喷
// sfg1991@163.com
// 2015-04-22

<think>我们正在讨论的是Java中对类似"Pn-Bn"(n为数字)这样的字符串进行排序的题。这种字符串常见于带有前缀和编号的标识符,例如"P1-B1"、"P2-B10"等。排序时需要特别注意数字部分的大小比较,避免出现字典序排序(如"P1-B10"排在"P1-B2"前面)的题。 ### 排序需求分析 假设我们有一组字符串:`["P3-B2", "P1-B10", "P1-B2", "P10-B1", "P2-B3"]`,期望的排序结果应该是: 1. 首先按前缀"P"后的数字升序 2. 同一项目内按"B"后的数字升序 因此正确排序结果应为:`["P1-B2", "P1-B10", "P2-B3", "P3-B2", "P10-B1"]` ### 实现方法 在Java中,我们可以通过自定义`Comparator`来实现这种复杂排序逻辑。具体步骤如下: #### 1. 使用正则表达式提取数字 我们可以用正则表达式匹配字符串中的数字部分。例如: ```java Pattern pattern = Pattern.compile("P(\\d+)-B(\\d+)"); ``` #### 2. 自定义比较器 ```java import java.util.Comparator; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PBComparator implements Comparator<String> { private static final Pattern PATTERN = Pattern.compile("P(\\d+)-B(\\d+)"); @Override public int compare(String s1, String s2) { Matcher m1 = PATTERN.matcher(s1); Matcher m2 = PATTERN.matcher(s2); if (m1.find() && m2.find()) { // 提取P后面的数字 int p1 = Integer.parseInt(m1.group(1)); int p2 = Integer.parseInt(m2.group(1)); // 先比较P后面的数字 if (p1 != p2) { return Integer.compare(p1, p2); } // 如果P后面的数字相同,再比较B后面的数字 int b1 = Integer.parseInt(m1.group(2)); int b2 = Integer.parseInt(m2.group(2)); return Integer.compare(b1, b2); } // 如果不符合模式,则按字典序比较 return s1.compareTo(s2); } } ``` #### 3. 使用示例 ```java import java.util.Arrays; public class Main { public static void main(String[] args) { String[] arr = {"P3-B2", "P1-B10", "P1-B2", "P10-B1", "P2-B3"}; Arrays.sort(arr, new PBComparator()); System.out.println(Arrays.toString(arr)); // 输出: [P1-B2, P1-B10, P2-B3, P3-B2, P10-B1] } } ``` ### 注意事项 1. **异常处理**:如果字符串不符合模式(比如缺少数字部分),上述代码会回退到字典序排序。根据实际需求,可能需要更严格的错误处理。 2. **性能考虑**:对于大量据排序,可以预处理提取数字并缓存,避免每次比较都执行正则匹配。 3. **数字格式**:如果数字前有前导零(如"P01"),需要特别注意,因为按整比较时"01"和"1"是相等的。如果存在这种情况,可能需要按字符串长度和内容综合处理。 ### 优化版本(缓存提取结果) 对于大规模据排序,我们可以提前解析字符串并存储数字对: ```java import java.util.Arrays; import java.util.Comparator; import java.util.regex.Matcher; import java.util.regex.Pattern; public class OptimizedPBComparator implements Comparator<String> { private static final Pattern PATTERN = Pattern.compile("P(\\d+)-B(\\d+)"); // 解析字符串,返回一个包含两个整数组[pNum, bNum],如果解析失败返回null private int[] parse(String s) { Matcher m = PATTERN.matcher(s); if (m.find()) { return new int[] { Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)) }; } return null; } @Override public int compare(String s1, String s2) { int[] nums1 = parse(s1); int[] nums2 = parse(s2); if (nums1 != null && nums2 != null) { // 先比较P数字 int cmpP = Integer.compare(nums1[0], nums2[0]); if (cmpP != 0) return cmpP; // 再比较B数字 return Integer.compare(nums1[1], nums2[1]); } // 至少有一个解析失败,按字典序 return s1.compareTo(s2); } public static void main(String[] args) { String[] arr = {"P3-B2", "P1-B10", "P1-B2", "P10-B1", "P2-B3"}; Arrays.sort(arr, new OptimizedPBComparator()); System.out.println(Arrays.toString(arr)); } } ``` ### 扩展讨论 如果字符串格式更复杂(例如包含多个数字段),可以扩展上述方法。核心思路仍然是: 1. 提取关键数字部分 2. 按优先级比较这些数字 ### 相关1. 如果排序键中的数字可能非常大(超出`int`范围),应如何修改比较器? 2. 如何实现降序排列或混合排序(如P数字升序,B数字降序)? 3. 当遇到非标准格式(如"Project10-Block2")时,如何增强比较器的容错性? </think>### Java中对"Pn-Bn"格式字符串排序的实现方法 在Java中对形如"Pn-Bn"(n为数字)的字符串进行排序时,需要特别注意数字部分的值排序(而非字典序排序)。以下是完整的实现方案: --- ### 1. 自定义比较器实现 核心思路:使用正则表达式提取数字部分,转换为整后进行值比较。 ```java import java.util.Arrays; import java.util.Comparator; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PnBnSorter { // 正则匹配 P<数字>-B<数字> 格式 private static final Pattern PATTERN = Pattern.compile("P(\\d+)-B(\\d+)"); public static void main(String[] args) { String[] data = {"P3-B2", "P1-B10", "P1-B2", "P10-B1", "P2-B3"}; // 自定义比较器 Comparator<String> comparator = (s1, s2) -> { Matcher m1 = PATTERN.matcher(s1); Matcher m2 = PATTERN.matcher(s2); if (m1.find() && m2.find()) { // 提取P后的数字 int p1 = Integer.parseInt(m1.group(1)); int p2 = Integer.parseInt(m2.group(1)); if (p1 != p2) return Integer.compare(p1, p2); // 优先按P数字排序 // P数字相同再比较B数字 int b1 = Integer.parseInt(m1.group(2)); int b2 = Integer.parseInt(m2.group(2)); return Integer.compare(b1, b2); } return s1.compareTo(s2); // 格式错误时退回字典序 }; Arrays.sort(data, comparator); System.out.println(Arrays.toString(data)); // 输出: [P1-B2, P1-B10, P2-B3, P3-B2, P10-B1] } } ``` --- ### 2. 关键实现解析 1. **正则表达式分解** ```java Pattern.compile("P(\\d+)-B(\\d+)") ``` - `\\d+` 匹配连续数字 - 捕获组 `( )` 提取P和B的数字部分 2. **多级比较逻辑** - 优先比较 `P` 后的数字(`group(1)`) - 当P数字相同时,再比较 `B` 后的数字(`group(2)`) - 使用 `Integer.compare()` 确保值排序(避免字典序题如 "B10" < "B2") 3. **异常处理** - 建议添加 `try-catch` 处理数字解析异常 - 格式不匹配时可返回默认字典序排序 --- ### 3. 示例输入输出 输入数组: ```java ["P3-B2", "P1-B10", "P1-B2", "P10-B1", "P2-B3"] ``` 排序后输出: ``` [P1-B2, P1-B10, P2-B3, P3-B2, P10-B1] ``` - `P1-B2` 排在 `P1-B10` 前(2 < 10) - `P10-B1` 排在最后(10 > 3) --- ### 4. 扩展优化建议 1. **支持可变前缀** 修改正则表达式为 `([A-Z]+)(\\d+)-([A-Z]+)(\\d+)` 可支持其他前缀(如 "AX10-BY3") 2. **处理前导零** 在 `parseInt` 前添加处理逻辑: ```java String numStr = m1.group(1).replaceFirst("^0+(?!$)", ""); ``` 3. **封装为工具类** ```java public class CustomSorter { public static void sortPnBn(String[] array) { // 实现比较逻辑 } } ``` --- ### 相关1. 如何实现降序排列(先按P数字降序,再按B数字降序)? 2. 当字符串格式包含非数字字符(如 "P1A-B2")时,如何增强比较器的容错性? 3. 对于超大据集的排序(100万+记录),如何优化比较器的性能? 4. 在Spring Boot项目中,如何将这种自定义排序集成到JPA查询中?[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值