二、题目
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
1、思路
大家首先想到的是顺序扫描法,但是这种方法的时间复杂度是O(n^2)。接着大家又会考虑用哈希表的方法,但是空间复杂度不是O(1)。
应该怎么做才能即满足时间复杂度是O(n)又满足空间复杂度是O(1)的要求呢?
我们可以想一想“异或”运算的一个性质,我们直接举例说明。
举例:{2,4,3,6,3,2,5,5}
这个数组中只出现一次的两个数分别是4和6。怎么找到这个两个数字呢?
我们先不看找到俩个的情况,先看这样一个问题,如何在一个数组中找到一个只出现一次的数字呢?比如数组:{4,5,5},唯一一个只出现一次的数字是4。
我们知道异或的一个性质是:任何一个数字异或它自己都等于0。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字。比如数组{4,5,5},我们先用数组中的第一个元素4(二进制形式:0100)和数组中的第二个元素5(二进制形式:0101)进行异或操作,0100和0101异或得到0001,用这个得到的元素与数组中的三个元素5(二进制形式:0101)进行异或操作,0001和0101异或得到0100,正好是结果数字4。这是因为数组中相同的元素异或是为0的,因此就只剩下那个不成对的孤苦伶仃元素。
现在好了,我们已经知道了如何找到一个数组中找到一个只出现一次的数字,那么我们如何在一个数组中找到两个只出现一次的数字呢?如果,我们可以将原始数组分成两个子数组,使得每个子数组包含一个只出现一次的数字,而其他数字都成对出现。这样,我们就可以用上述方法找到那个孤苦伶仃的元素。
我们还是从头到尾一次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数组的异或结果。因为其他数字都出现了两次,在异或中全部抵消了。由于两个数字肯定不一样,那么异或的结果肯定不为0,也就是说这个结果数组的二进制表示至少有一个位为1。我们在结果数组中找到第一个为1的位的位置,记为第n位。现在我们以第n位是不是1为标准把元数组中的数字分成两个子数组,第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0。
举例:{2,4,3,6,3,2,5,5}
我们依次对数组中的每个数字做异或运行之后,得到的结果用二进制表示是0010。异或得到结果中的倒数第二位是1,于是我们根据数字的倒数第二位是不是1分为两个子数组。第一个子数组{2,3,6,3,2}中所有数字的倒数第二位都是1,而第二个子数组{4,5,5}中所有数字的倒数第二位都是0。接下来只要分别两个子数组求异或,就能找到第一个子数组中只出现一次的数字是6,而第二个子数组中只出现一次的数字是4。
class Solution {
public:
void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
int length = data.size();
if(length < 2){
return;//一定要出现3次数
}
// 对原始数组每个元素求异或
int resultExclusiveOR = 0;//初始化
for(int i = 0; i < length; ++i){
resultExclusiveOR ^= data[i];//求异或,其他数字出现了两次,在异或中被抵消了,
由于两个数字肯定不一样,那么异或的结果肯定不为0,也就是说这个结果数组的二进制表示至少有一个位为1
}
unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);
*num1 = *num2 = 0;
for(int j = 0; j < length; j++){
if(IsBit1(data[j], indexOf1)){//我们在结果数组中找到第一个为1的位的位置,记为第n位。
*num1 ^= data[j];
}
else{
*num2 ^= data[j];
}
}
}//以第n位是不是1为标准把元数组中的数字分成两个子数组,第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0。
private:
// 找到二进制数num第一个为1的位数,比如0010,第一个为1的位数是2。
unsigned int FindFirstBitIs1(int num){
unsigned int indexBit = 0;
// 只判断一个字节的
while((num & 1) == 0 && (indexBit < 8 * sizeof(unsigned int))){
num = num >> 1;
indexBit++;
}
return indexBit;
}
// 判断第indexBit位是否为1
bool IsBit1(int num, unsigned int indexBit){
num = num >> indexBit;
return (num & 1);//接下来只要分别两个子数组求异或,就能找到第一个子数组中只出现一次的数字是6,而第二个子数组中只出现一次的数字是4。
}
};
class Solution:
# 返回[a,b] 其中ab是出现一次的两个数字
def FindNumsAppearOnce(self, array):
# write code here
if len(array) <= 0:
return []
resultExclusiveOR = 0
length = len(array)
for i in array:
resultExclusiveOR ^= i
firstBitIs1 = self.FindFisrtBitIs1(resultExclusiveOR)
num1, num2 = 0, 0
for i in array:
if self.BitIs1(i, firstBitIs1):
num1 ^= i
else:
num2 ^= i
return num1, num2
def FindFisrtBitIs1(self, num):
indexBit = 0
while num & 1 == 0 and indexBit <= 32:
indexBit += 1
num = num >> 1
return indexBit
def BitIs1(self, num, indexBit):
num = num >> indexBit
return num & 1
举例说明:{2,3,1,0,2,5,3}
个人感觉最优的方法:题目里写了数组里数字的范围保证在0 ~ n-1 之间,所以可以利用现有数组设置标志,当一个数字被访问过后,可以设置对应位上的数 + n,之后再遇到相同的数时,会发现对应位上的数已经大于等于n了,那么直接返回这个数即可。
- 记数组中存在的唯一的两个数字分别为a,b
- 首先以二进制的角度去看所有的数字,每一位均为0,1组成
- 对所有数字进行个位上的异或,成对相同的数字结果为0,每一位上都这样异或依次,所以最终每一位上存在1的则必然是因为a,b在这一位上不同
- 根据最终结果上存在‘1’的这一位,将原数组分为两组,一组‘1’,一组‘0‘,
- 两组数字再分别异或,最终两个结果就是a,b;
-
class Solution { public: void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) { int num=0; for(int i=0;i < data.size();i++){ num^=data[i];//所有数异或,结果为不同的两个数字的异或,0与任何数字异或等于数字本身 }//最后肯定不是0,这个1出现的位置就是AB不同的地方,以这个地方来分界,划分为两个数组 int count=0;//标志位,记录num中的第一个1出现的位置 for(;count < data.size();count++){ if((num&(1<<count))!=0){ break; } } int num_1 = 0; int num_2 = 0; for(int i=0; i < data.size(); i++){ if((data[i]&(1<<count))==0){//标志位为0的为一组,异或后必得到一个数字(这里注意==的优先级高于&,需在前面加()) num_1^=data[i]; }else{ num_2^=data[i];//标志位为1的为一组 } } *num1 = num_1; *num2 = num_2;//最后输出的NUM就是那个数字 } };
#include<stdio.h>
#include<stack>
#include<map>
using
namespace
std;
int
main(){
stack<
char
> s;
map<
char
,
char
> book;
book[
')'
]=
'('
,book[
']'
]=
'['
,book[
'}'
]=
'{'
;
char
in[100];
scanf
(
"%s"
,in);
int
flag=1,i;
for
(i=0;in[i]!=
'\0'
;i++)
if
(in[i]==
'('
||in[i]==
'['
||in[i]==
'{'
) s.push(in[i]);
else
{
if
(s.empty()||s.top()!=book[in[i]]){
flag=0;
break
;
}
s.pop();
}
printf
(
"%s"
,s.empty()&&flag?
"true"
:
"false"
);
}
- 输入一组未排序的整数,其中一个数字只出现一次,剩下的所有数字都出现了三次。找出这个只出现一次的数字。例如输入: [1,2,2,2,3,3,3],输出为1
- n&(1<<i)是用来判断n的第i位是否为1。i>>1表示i右移1位,1<<i表示1左移i位,相当于第i位为1,其他位为0的整数。因此n&(1<<i)可以判断整数n的第i位是否为1。
-
import java.util.*; public class Main{ public static void main(String[] args){ Scanner sc=new Scanner(System.in); int n=sc.nextInt();//N是数组里面的个数 int[] a=new int[n]; int sum=0; for(int i=0;i<n;i++){ a[i]=sc.nextInt();//输入每一个数字 } int x=0; for(int i=0;i<32;i++){//查看32个位置上面的数字为1的个数是否是3的倍数多1,整数不超过32次 int bit=1<<i;//设置位置,表示第I位为1 int count=0; for(int j=0;j<n;j++){ if((bit&a[j])!=0)//判断一个整数的第I位是不是1,第I位是1 就加一次个数 count++; } if(count%3==1)//代表单独出现的那个数字在 1<<i(第I位是1)这个位置上有出现1,故将其相加 x=x|bit; } System.out.println(x); } }//x=x&(x-1); 这行代码什么意思,&的这个用法是什么意思? //这是“位运算”中的一种很经典的用法,“&”是“位于”的意思。它具体点的意思就是把x的二进制表示数最右边的一个1变成0 例如: e1: x = 01001000 x-1 = 01000111 x&(x-1)= 01000000 e2: x = 01001001 x-1 = 01001000
有一个长度为N的序列。一开始,这个序列是1, 2, 3,... n - 1, n的一个排列。
对这个序列,可以进行如下的操作:
每次选择序列中k个连续的数字,然后用这k个数字中最小的数字替换这k个数字中的每个数字。
我们希望进行了若干次操作后,序列中的每个数字都相等。请你找出需要操作的最少次数。
输入描述:
第一行:两个数字n, k,含义如题,满足2 <= k <= n <= 105;
第二行:n个数字,是1, 2, 3,...n的一个排列。
输出描述:
一个整数,表示最少的次数。
示例1
输入
复制
2 2 2 1
输出
复制
1
示例2
输入
复制
4 3 1 2 3 4
输出
复制
2
方法1:
-
对于结果只有一个数字,输入是N个不同的数字,那么肯定有N-1次改变,每次要改变K-1次,向上取整。
序列的最终结果为一个数,那么长度为n的序列有n-1个数字需要改变;每次取k个数字,则最多有k-1个数字需要改变,所以次数为(n-1)/(k-1)向上取整。#include<bits/stdc++.h>
using namespace std;
int main(){
int n,k;
while(cin>>n>>k){
for(int i = 0;i<n;i++){
int x;
cin>>x;
}
cout<<ceil((double)(n-1)/(k-1))<<endl;
}
system("pause");
return 0;
}
方法2:
第一步:从n个数字中不重复的取k个数字,则可取n/k次,剩余(n/k+n%k)个数字不同;
第二步:从n/k+n%k个数中取k个数字,重复第一步,直到剩余数字<=k个。
#include<iostream>
#include <string>
usingnamespacestd;
intmain()
{
intn,k;
intcycle=0,num=0;
string s;
cin>>n>>k;
getline(cin,s);
while((n>k))
{
cycle=n/k;
n=cycle+n%k;
num+=cycle;
}
num++;
cout<<num;
}
-
import java.util.Scanner;
public class Main {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public
static
void
main(String[] args) {
int
result =
0
;
Scanner sc =
new
Scanner(System.in);
int
n = sc.nextInt();
int
l = sc.nextInt();
if
(n==l){
System.out.println(
1
);//输入5个数字,每次取5个,肯定要改变1次搞定
}
else
{
result = (n-l)/(l-
1
) +
1
;//向上取整
if
((n-l)%(l-
1
)>
0
) {
result++;
}
System.out.println(result);
}
}
}
-
对于一个由0..n的所有数按升序组成的序列,我们要进行一些筛选,每次我们取当前所有数字中从小到大的第奇数位个的数,并将其丢弃。重复这一过程直到最后剩下一个数。请求出最后剩下的数字。
输入描述:
每组数据一行一个数字,为题目中的n(n小于等于1000)。
输出描述:
一行输出最后剩下的数字。
示例1
输入
复制
500
输出
复制
255
因为是从0开始,所以第一轮移走的是二进制下最右边为0的位置(从0开始的偶数位置)上的数,然后我们发现第二轮各个number的位置等于number/2,即从number位置到number>>1位置,这时候我们依然移走二进制下最右边为0的位置(1(01) 5(101) 9(1001) ……它们第二轮对应的位置是0, 2, 4),最后剩一个数肯定是0到n中二进制下1最多的那个数,因为它每次的位置都是奇数位置。代-
//特殊思路,每次删除所在数组位置的二进制最右端为0的元素。如0(0)2(10)4(100)
//剩余的元素1(01)3(11)5(101)下一次其位置变成了之前位置左移一次后的
// 1(1) 3(10) 5(10) 然后继续按之前规则删除最右端为0的元素。故原始序列中,谁的//二进制下从右往左数,1最多,则最后删除,因每次删除移位后,最右端仍然为1,会保留
#include<iostream>
using
namespace
std;
int
main(){
int
n;
while
( cin >> n ){
int
b = 1;
while
( b <= n )
/*b = (b<<1) + 1;//或者 用*/
b = b*2 +1;
cout << (b>>1) << endl;
}
}
-
这个表示移位运算,就是把num转换成二进制表示后所有位向后移动一位,高位补0
-
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
-
1、思路
还可以把当前序列当成是一个下标和下标对应值是相同的数组(时间复杂度为O(n),空间复杂度为O(1)); 遍历数组,判断当前位的值和下标是否相等:
- 若相等,则遍历下一位;
- 若不等,则将当前位置i上的元素和a[i]位置上的元素比较:若它们相等,则找到了第一个相同的元素;若不等,则将它们两交换。换完之后a[i]位置上的值和它的下标是对应的,但i位置上的元素和下标并不一定对应;重复2的操作,直到当前位置i的值也为i,将i向后移一位,再重复2。
- 0(索引值)和2(索引值位置的元素)不相等,并且2(索引值位置的元素)和1(以该索引值位置的元素2为索引值的位置的元素)不相等,则交换位置,数组变为:{1,3,2,0,2,5,3};
- 0(索引值)和1(索引值位置的元素)仍然不相等,并且1(索引值位置的元素)和3(以该索引值位置的元素1为索引值的位置的元素)不相等,则交换位置,数组变为:{3,1,2,0,2,5,3};
- 0(索引值)和3(索引值位置的元素)仍然不相等,并且3(索引值位置的元素)和0(以该索引值位置的元素3为索引值的位置的元素)不相等,则交换位置,数组变为:{0,1,2,3,2,5,3};
- 0(索引值)和0(索引值位置的元素)相等,遍历下一个元素;
- 1(索引值)和1(索引值位置的元素)相等,遍历下一个元素;
- 2(索引值)和2(索引值位置的元素)相等,遍历下一个元素;
- 3(索引值)和3(索引值位置的元素)相等,遍历下一个元素;
- 4(索引值)和2(索引值位置的元素)不相等,但是2(索引值位置的元素)和2(以该索引值位置的元素2为索引值的位置的元素)相等,则找到了第一个重复的元素。
-
class Solution { public: // Parameters: // numbers: an array of integers // length: the length of array numbers // duplication: (Output) the duplicated number in the array number // Return value: true if the input is valid, and there are some duplications in the array number // otherwise false bool duplicate(int numbers[], int length, int* duplication) { // 非法输入 if(numbers == NULL || length <= 0){ return false; } // 非法输入 for(int i = 0; i < length; i++){ if(numbers[i] < 0 || numbers[i] > length - 1){ return false; } } // 遍历查找第一个重复的数 for(int i = 0; i < length; i++){ while(numbers[i] != i){ if(numbers[i] == numbers[numbers[i]]){ *duplication = numbers[i]; return true; } swap(numbers[i], numbers[numbers[i]]); } } return false; } };
class Solution:
# 这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
# 函数返回True/False
def duplicate(self, numbers, duplication):
# write code here
n = len(numbers)
if n == 0:
return False
for i in range(n):
if numbers[i] < 0 or numbers[i] > n-1:
return False
for i in range(n):
while numbers[i] != i:
if numbers[i] == numbers[numbers[i]]:
duplication[0] = numbers[i]
return True
numbers[numbers[i]], numbers[i] = numbers[i], numbers[numbers[i]]
return False
-
class Solution:
# 这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
# 函数返回True/False
def duplicate(self, numbers, duplication):
# write code here
n = len(numbers)
if n == 0:
return False
for i in range(n):
index = numbers[i]
if index >= n:
index -= n
if numbers[index] >= n:
duplication[0] = index
return True
numbers[index] += n
return False
class Solution: # 这里要特别注意~找到任意重复的一个值并赋值到duplication[0] # 函数返回True/False def duplicate(self, numbers, duplication): # write code here n = len(numbers) if n == 0: return False for i in range(n): index = numbers[i]//把每个位置的值映射到数组的下标,这样有重复的数字的话就会对这个位置访问两次以上,如果我们在第一次的时候进行一些操作,那么第二次访问时肯定就不一样了,就能找到我们想要的结果。 if index >= n: index -= n//类似于桶排序,把把取模,归一化到0到N的范围 if numbers[index] >= n: duplication[0] = index//如果通过这个下标访问的数字大于N,肯定被操作了,就是重复的数字 return True numbers[index] += n//如果没有大于N,说明是第一次访问,我们把它加上N,作为一个标记 return False