BM54-三数之和

文章讨论了一种解决数组中寻找三元组(a,b,c)使得a+b+c=0的问题,主要介绍了两种方法:双指针法和哈希表法。双指针法首先对数组排序,然后遍历数组,使用双指针在剩余部分寻找和为目标值的数对,同时处理重复元素。哈希表法同样先排序,然后通过哈希表来快速查找目标元素的互补值,以达到去重的目的。这两种方法的时间复杂度都是O(n^2),但哈希表法在去重上更有效。

题目

给出一个有n个元素的数组S,S中是否有元素a,b,c满足a+b+c=0?找出数组S中所有满足条件的三元组。

数据范围:0≤n≤1000,数组中各个元素值满足 ∣val∣≤100。

空间复杂度:O(n^2),时间复杂度 O(n^2)。

注意:

  1. 三元组(a、b、c)中的元素必须按非降序排列。(即a≤b≤c)
  2. 解集中不能包含重复的三元组。

例如,给定的数组 S = {-10 0 10 20 -10 -40},解集为(-10, -10, 20),(-10, 0, 10)。

示例1

输入:[0]

返回值:[]

示例2

输入:[-2,0,1,1,2]

返回值:[[-2,0,2],[-2,1,1]]

示例3

输入:[-10,0,10,20,-10,-40]

返回值:[[-10,-10,20],[-10,0,10]]


思路1:双指针

实现版本1:

直接找三个数字之和为某个数,太麻烦了,我们是不是可以拆分一下:如果找到了某个数a,要找到与之对应的另外两个数,三数之和为0,那岂不是只要找到另外两个数之和为−a?这就方便很多了。

因为三元组内部必须是有序的,因此可以优先对原数组排序,这样每次取到一个最小的数为a,只需要在后续数组中找到两个之和为−a就可以了,我们可以用双指针缩小区间,因为太后面的数字太大了,就不可能为−a,可以舍弃。

具体做法:

  • step 1:排除边界特殊情况。
  • step 2:既然三元组内部要求非降序排列,那我们先得把这个无序的数组搞有序了,使用sort函数优先对其排序。
  • step 3:得到有序数组后,遍历该数组,对于每个遍历到的元素假设它是三元组中最小的一个,那么另外两个一定在后面。
  • step 4:需要三个数相加为0,则另外两个数相加应该为上述第一个数的相反数,我们可以利用双指针在剩余的子数组中找有没有这样的数对。双指针指向剩余子数组的首尾,如果二者相加为目标值,那么可以记录,而且二者中间的数字相加可能还会有。
  • step 5:如果二者相加大于目标值,说明右指针太大了,那就将其左移缩小,相反如果二者相加小于目标值,说明左指针太小了,将其右移扩大,直到两指针相遇,剩余子数组找完了。

注:对于三个数字都要判断是否相邻有重复的情况,要去重。


代码1.1

import java.util.ArrayList;
import java.util.Arrays;

public class Solution {
    public ArrayList<ArrayList<Integer>> threeSum(int[] num) {
        ArrayList<ArrayList<Integer> > res = new ArrayList<ArrayList<Integer>>();
        int n = num.length;

        //不够三元组
        if(n < 3) {
            return res;
        }

        //排序
        Arrays.sort(num);

        for(int i = 0; i < n - 2; i++) {
            if(i != 0 && num[i] == num[i - 1]) {
                continue;
            }
            //后续的收尾双指针
            int left = i + 1;
            int right = n - 1;
            //设置当前数的负值为目标
            int target = -num[i];

            while(left < right) {
                //双指针指向的二值相加为目标,则可以与num[i]组成0
                if(num[left] + num[right] == target) {
                    ArrayList<Integer> temp = new ArrayList<Integer>();
                    temp.add(num[i]);
                    temp.add(num[left]);
                    temp.add(num[right]);
                    res.add(temp);
                    while(left + 1 < right && num[left] == num[left + 1]) {
                        //去重
                        left++;
                    }
                    while(right - 1 > left && num[right] == num[right - 1]) {
                        //去重
                        right--;
                    }
                    //双指针向中间收缩
                    left++;
                    right--;
                } else if(num[left] + num[right] > target) {  //双指针指向的二值相加大于目标,右指针向左
                    right--;
                } else {  //双指针指向的二值相加小于目标,左指针向右
                    left++;
                }
            }
        }
        return res;
    }
}
  • 时间复杂度:O(n^2),排序的复杂度为O(nlog2n),查找三元组的复杂度为O(n2)。
  • 空间复杂度:O(1),res属于必要空间,不属于额外空间,无其他辅助空间。

实现版本2:

  1. 对数组长度进行特判。
  2. 排序。
  • num[i] > 0说明后面的三数和不可能等于0。
  • 对于重复元素跳过。
  • 左指针left = i + 1,右指针right = len - 1
  • nums[i] + nums[left] + nums[right] == 0执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 left,right 移到下一位置,寻找新的解。
  • 如果和<0,left++;
  • 如果和>0,right--;


代码1.2

import java.util.*;

public class Solution {
    public ArrayList<ArrayList<Integer>> threeSum(int[] num) {
        //存放最终答案的二维数组
        ArrayList<ArrayList<Integer>> ans = new ArrayList<>();
        int len = num.length;

        //特判:长度<3的数组不满足条件
        if (len < 3) {
            return ans;
        }

        //排序O(nlogn)
        Arrays.sort(num);

        for (int i = 0; i < len; i++) {
            //如果nums[i]已经大于0,就没必要继续往后了,因为和就是0啊
            if (num[i] > 0) {
                return ans;
            }
            //注意考虑越界i>0,主要功能是排除重复值
            if (i > 0 && num[i] == num[i - 1]) {
                continue;
            }
            //声明指针
            int cur = num[i];
            int left = i + 1;
            //从尾部开始
            int right = len - 1;
            
            while (left < right) {
                //满足条件的三数和
                int tp_ans = cur + num[left] + num[right];
                //如果已经找到和为0
                if (tp_ans == 0) {
                    //创建一个数组,并将满足条件的三元素放进去
                    ArrayList<Integer> list = new ArrayList<>();
                    list.add(cur);
                    list.add(num[left]);
                    list.add(num[right]);
                    //将最终的结果存入答案数组ans中
                    ans.add(list);
                    //判断是left指针指向是否重复
                    while (left < right && num[left] == num[left + 1]) {
                        left++;
                    }
                    //判断是right指针指向是否重复
                    while (left < right && num[right] == num[right - 1]) {
                        right--;
                    }
                    //移动指针
                    left++;
                    right--;
                } else if (tp_ans < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return ans;
    }
}
  • 时间复杂度:O(n^2)。
  • 空间复杂度:O(1)。

思路2:哈希表

利用哈希表特性,对符合条件的三元组进行去重处理。


代码2

import java.util.*;

public class Solution {
    public ArrayList<ArrayList<Integer>> threeSum(int[] num) {
        //存放答案的集合
        ArrayList<ArrayList<Integer>> ans = new ArrayList<>();
        int len =  num.length;

        //排序O(nlogn)
        Arrays.sort(num);

        //哈希表去重
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < len; i++) {
            map.put(num[i], i);
        }

        //若干变量声明
        int L, M, R;
        for (int i = 0; i < len; i = map.get(L) + 1) {
            //指定L的值
            L = num[i];
            //注意里层循环从i+1开始
            for (int j = i + 1; j < len; j = map.get(M) + 1) {
                M = num[j];
                //注意一下,这里是个容易错的细节..
                R = -L - M;
                if (R < M) {
                    break;
                }
                if (map.get(R) != null && map.get(R) > j) {
                    //创建一个数组,并将满足条件的三元素放进去
                    ArrayList<Integer> list = new ArrayList<Integer>();
                    list.add(L);
                    list.add(M);
                    list.add(R);
                    //将最终的结果存入答案数组ans中
                    ans.add(list);
                    //ans.add(Arrays.asList(L,M,R)); 不知道为什么这种写法在牛客平台编译过不了
                }
            }
        }
        return ans;
    }
}

<think>我们正在讨论BMP图像格式。用户询问的是BMP文件中前54个字节的具体含义。根据引用[1],BMP文件结构分为四个部分:位图文件头、位图信息头、调色板和位图数据。其中,前54个字节通常由位图文件头和位图信息头组成(在没有调色板的24位位图中)。具体如下: 1. 位图文件头(BITMAPFILEHEADER)占14个字节。 2. 位图信息头(BITMAPINFOHEADER)占40个字节(这是最常见的一种,还有其他类型,但标准BMP通常用这个)。 因此,14+40=54字节。下面我们详细解释这54个字节: ### 一、位图文件头(14字节) 位图文件头的结构如下(括号内为字节偏移,从0开始): - 0-1字节:文件类型,必须是"BM"(即0x4D42,注意小端存储,所以实际存储顺序是0x42, 0x4D)。 - 2-5字节:整个文件的大小(以字节为单位)。这是一个32位无符号整数(小端存储)。 - 6-9字节:保留字段,一般设置为0。 - 10-13字节:从文件开始到位图数据开始之间的字节偏移量(也就是位图文件头+位图信息头+调色板的长度,在没有调色板的24位图中通常为54)。 ### 二、位图信息头(40字节) 位图信息头(BITMAPINFOHEADER)的结构如下(括号内为字节偏移,从14开始): - 14-17字节:信息头的大小(通常为40,即0x28000000,小端存储为0x28,0x00,0x00,0x00)。 - 18-21字节:图像的宽度(以像素为单位,有符号整数,但通常为正数)。 - 22-25字节:图像的高度(以像素为单位,有符号整数。正数表示图像是倒置的(即第一行在文件末尾),负数表示图像是正立的。通常BMP文件是倒置的)。 - 26-27字节:颜色平面数,总是设置为1。 - 28-29字节:每个像素的位数(常见的有1,4,8,16,24,32)。例如24位图就是24(0x1800,小端存储为0x18,0x00)。 - 30-33字节:压缩方式。0表示不压缩(BI_RGB)。其他值(如BI_BITFIELDS)表示有压缩或特殊格式(如16/32位位图使用位域)。 - 34-37字节:图像数据的大小(以字节为单位)。如果是不压缩的RGB,可以设置为0。 - 38-41字节:水平分辨率(像素/米,有符号整数)。 - 42-45字节:垂直分辨率(像素/米,有符号整数)。 - 46-49字节:调色板中的颜色数。如果为0,则表示使用所有颜色(对于24位位图,通常为0)。 - 50-53字节:重要颜色数。通常为0,表示所有颜色都重要。 ### 注意: - 以上所有多字节整数都是小端(Little-Endian)存储。 - 在没有调色板的24位位图中,文件头14字节+信息头40字节=54字节,紧接着就是位图数据。 ### 举例说明 假设有一个24位BMP图像,宽100像素,高100像素,没有调色板。那么: 1. 文件头(14字节): - 字节0-1: 0x42, 0x4D('BM') - 字节2-5: 文件大小(由于图像数据大小为100*100*3=30000字节,加上54字节头,共30054字节。所以这4个字节为0x66,0x75,0x00,0x00(小端,即0x00007566=30054)) - 字节6-9: 0,0,0,0(保留) - 字节10-13: 0x36,0x00,0x00,0x00(即54,因为后面紧跟图像数据) 2. 信息头(40字节): - 字节14-17: 0x28,0x00,0x00,0x00(40) - 字节18-21: 0x64,0x00,0x00,0x00(100的十六进制是0x64) - 字节22-25: 0x64,0x00,0x00,0x00(100,注意正数表示图像倒置) - 字节26-27: 0x01,0x00(颜色平面数1) - 字节28-29: 0x18,0x00(24位) - 字节30-33: 0x00,0x00,0x00,0x00(不压缩) - 字节34-37: 0x00,0x00,0x00,0x00(可以设为0) - 字节38-41: 0x00,0x00,0x00,0x00(水平分辨率,可设为0) - 字节42-45: 0x00,0x00,0x00,0x00(垂直分辨率,可设为0) - 字节46-49: 0x00,0x00,0x00,0x00(调色板颜色数,0表示没有调色板) - 字节50-53: 0x00,0x00,0x00,0x00(重要颜色数,0) ### 补充说明 - 对于16位或32位的BMP,信息头可能仍然是40字节,但压缩方式字段可能设置为3(BI_BITFIELDS),此时在信息头后面会有额外的16或12字节的掩码(分别用于16位和32位)。这样文件头就会超过54字节(例如16位BMP:14+40+12=66字节)。但用户问题中提到的54字节是针对最常见的24位无压缩BMP。 ### 总结 BMP图像的前54字节(对于24位标准BMP)包含: 1. 14字节文件头:标识文件类型、大小、数据偏移等。 2. 40字节信息头:包含图像的宽度、高度、位深度、压缩方式等信息。 [^1] ### 相关问题 1. 如何读取一个BMP文件的文件头和信息头? 2. BMP图像数据区的存储方式有什么特点? 3. 除了24位BMP,其他位深度(如1位、8位、32位)的BMP文件结构有什么不同? </think>### BMP图像前54字节详解 BMP文件的前54字节包含**位图文件头**(14字节)和**位图信息头**(40字节),以下是逐字节解析(基于24位无压缩BMP的标准结构): #### 一、位图文件头(14字节,偏移量0x00-0x0D) | 偏移量 | 字节数 | 数据类型 | 含义 | 示例值(小端序) | |--------|--------|----------------|----------------------|--------------------| | 0x00 | 2 | `char[2]` | 文件标识符("BM") | `0x42 0x4D` | | 0x02 | 4 | `uint32_t` | 文件总大小(字节) | 文件实际大小 | | 0x06 | 4 | `uint32_t` | 保留字段(恒为0) | `0x00 0x00 0x00 0x00` | | 0x0A | 4 | `uint32_t` | **位图数据偏移量** | `0x36 0x00 0x00 0x00`(54) | > **关键点**: > - `0x0A-0x0D` 的偏移量值恒为 **54**(即 `0x36`),表示从文件起始到位图数据的距离[^1]。 --- #### 二、位图信息头(40字节,偏移量0x0E-0x35) | 偏移量 | 字节数 | 数据类型 | 含义 | 示例值(小端序) | |--------|--------|----------------|----------------------|--------------------| | 0x0E | 4 | `uint32_t` | 信息头大小(固定40) | `0x28 0x00 0x00 0x00`(40) | | 0x12 | 4 | `int32_t` | **图像宽度(像素)** | 宽度值(如100像素) | | 0x16 | 4 | `int32_t` | **图像高度(像素)** | 高度值(正数表示倒序存储) | | 0x1A | 2 | `uint16_t` | 颜色平面数(恒为1) | `0x01 0x00` | | 0x1C | 2 | `uint16_t` | **每像素位数** | `0x18 0x00`(24位)| | 0x1E | 4 | `uint32_t` | 压缩方式(0=无压缩) | `0x00 0x00 0x00 0x00` | | 0x22 | 4 | `uint32_t` | 位图数据大小(字节) | 图像数据实际大小 | | 0x26 | 4 | `int32_t` | 水平分辨率(像素/米)| 可选(常为0) | | 0x2A | 4 | `int32_t` | 垂直分辨率(像素/米)| 可选(常为0) | | 0x2E | 4 | `uint32_t` | 调色板颜色数(0=全部)| `0x00 0x00 0x00 0x00` | | 0x32 | 4 | `uint32_t` | 重要颜色数(常为0) | `0x00 0x00 0x00 0x00` | > **关键点**: > 1. **高度值符号**:正数表示图像倒序存储(数据区首行对应屏幕最后一行);负数表示正序存储。 > 2. **每像素位数**: > - `0x18`(24):RGB三通道各8位(共24位); > - `0x20`(32):RGBA四通道(含透明度)。 > 3. **压缩方式**: > - `0`:无压缩(BI_RGB); > - `3`:BITFIELDS(用于16/32位位图)。 --- #### 三、数据示例(宽度100×高度60的24位BMP) ```plaintext 文件头(0x00-0x0D): 42 4D 66 75 00 00 00 00 00 00 36 00 00 00 ↑ ↑ ↑ ↑ "BM" 文件大小30054 保留字段 数据偏移=54 信息头(0x0E-0x35): 28 00 00 00 64 00 00 00 3C 00 00 00 01 00 18 00 ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 40 宽100 高60 1平面 24位 00 00 00 00 00 84 03 00 00 00 00 00 00 00 00 00 ↑ ↑ ↑ 无压缩 数据大小230400 分辨率=0 00 00 00 00 00 00 00 00 ↑ 调色板颜色数=0 ``` > **计算**:数据大小 = 宽×高×3(100×60×3=18,000字节)[^1][^4]。 --- ### 总结 1. **54字节的组成**: - 前14字节:文件标识、大小、数据偏移量; - 后40字节:图像尺寸、位深度、压缩方式等元数据。 2. **核心字段**: - 数据偏移量(0x0A):恒指向54字节后; - 宽/高(0x12/0x16):决定像素阵列尺寸; - 位深度(0x1C):确定颜色模式(24位=RGB,32位=RGBA)。 [^1] [^4]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值