一、预热
在开始介绍前,我们先了解一些算法相关概念
1.1时间复杂度
我们想要知道一个算法的「时间复杂度」,很多人首先想到的的方法就是把这个算法程序运行一遍,那么它所消耗的时间就自然而然知道了。
这种方式可以吗?当然可以,不过它也有很多弊端。
这种方式非常容易受运行环境的影响,在性能高的机器上跑出来的结果与在性能低的机器上跑的结果相差会很大。而且对测试时使用的数据规模也有很大关系。再者,我们在写算法的时候,还没有办法完整的去运行。
因此,另一种更为通用的方法就出来了:「 大O符号表示法 」,即 T(n) = O(f(n))
在 大O符号表示法中,时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。
例子:
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
看上面的例子,假设每行代码的执行时间都是一样的,我们用 1颗粒时间 来表示,那么这个例子的第一行耗时是1个颗粒时间,第三行的执行时间是 n个颗粒时间,第四行的执行时间也是 n个颗粒时间(第二行和第五行是符号,暂时忽略),那么总时间就是 1颗粒时间 + n颗粒时间 + n颗粒时间 ,即 (1+2n)个颗粒时间,即: T(n) = (1+2n)颗粒时间,从这个结果可以看出,这个算法的耗时是随着n的变化而变化,因此,我们可以简化的将这个算法的时间复杂度表示为:T(n) = O(n)
另外对于无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
1.2空间复杂度
既然时间复杂度不是用来计算程序具体耗时的,那么我们也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的也是一个趋势,我们用 S(n) 来定义。
空间复杂度比较常用的有:
O(1)、O(n)、O(n²)
我们下面来看看:
(1) 空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1),如下:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
(2)空间复杂度 O(n)
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
。
同理,对于O(n²)即表示循环之中嵌套循环。
二. 方法介绍
方法1
func normalFindDifferent(){
let originArray = [2, 0, 1, 3, 5, 2, 10, 3, 5, 0,8,7,7,8,12,12,13,13,14,14,15,15,16,16]
var nums:[Int] = []
var times:Int = 0
for valueInt in originArray{
times = 0
for j in originArray{
if j == valueInt{
times += 1
}
}
if times == 1{
nums.append(valueInt)
}
}
print("不同的数为:\(nums)")
}
该方法中,利用times记录每个数的出现次数,其中times == 1即说明该数为单独存在。
时间复杂度:o(n * n)
空间复杂度:o(1)
方法2
func anotherFindDifferent(){
let originArray = [2, 0, 1, 3, 5, 2, 10, 3, 5, 0,8,7,7,8,12,12,13,13,14,14,15,15,16,16]
var dic:[Int:Int] = [:]
var nums:[Int] = []
for valueInt in originArray{
if let times = dic[valueInt]{
// dic.removeValue(forKey: valueInt)
dic[valueInt] = times + 1
}else{
dic[valueInt] = 1
}
}
for (key,value) in dic{
if value == 1{
nums.append(key)
}
}
print("不同的数为:\(nums)")
}
在该方法中,先自定义一个字典,该字典使用数组里面的整数做key,每个整数出现的次数作为value,然后对字典进行轮询,取出value为1的key值。
时间复杂度:o(1.5 * n)
空间复杂度:o(0.5 * n)
优化方法:
将dic[valueInt] = times + 1 改为 :dic.removeValue(forKey: valueInt),
即字典中仅存储value为1的值。
优化后的时间复杂度:o(n)
空间复杂度:o(1) ~ o(0.5n)
方法3
在介绍方法3之前,我们需要先了解一些位运算:
1.异或运算
异或的运算方法是一个二进制运算:
1^1=0
0^0=0
1^0=1
0^1=1
两者相等为0,不等为1.
而对于两个不相同并且不为0的数的异或算法如下: 如 7^10 = 13
0111 (7)
1010 (10)
-----
1101 = 13
2.与运算 与运算表示,两个都为1的时候,结果才为1,否则为0 1&1 = 1 0&1 = 0 0&0 = 0
3.>>(有符号右移位运算符) 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 如 0101 >> 2 = 0001,1110 1011 >> 2 = 1111 1010
方法介绍:
func findDifferent(){
let originArray = [2, 0, 1, 3, 5, 2, 10, 3, 5, 0,8,7,7,8,12,12,13,13,14,14,15,15,16,16]
var num1:Int = 0
var num2:Int = 0
var selectIndex:Int = 0
var temple:Int = 0
for valueInt in originArray{
temple ^= valueInt
}
//此时temple为两个单独数的异或值:11
//找出temple中第一个位数为1的位置,也就是这两个单数在二进制上第一个不同的位置。
for index in 0..<32{
if ((temple >> index) & 1) == 1{
selectIndex = index
break
}
}
//此处循环,就是将originArray在selectIndex的二进制位数上分成0和1两部分,此时分开的两部分分别包含一个单数,然后对这两部分分别做异或算法,则可以获得对应的单数
for value in originArray{
if ((value >> selectIndex) & 1) == 1{
num1 ^= value
}else{
num2 ^= value
}
}
print("不同的数为:\(num1),\(num2)")
}
比如:array = [3,4,5,6,4,3],换算成二进制为[0011,0100,0101,0110,0100,0011]
其中5和6的异或结果为:0011,其中第0位为1
则:(value >> 0) & 1) == 1 的有:0011(3),0101(5),0011(3)
以上方法的 时间复杂度:o(2n)
空间复杂度:o(1)