数组系列文章也到尾声了,这是数组系列的最后一篇文章,后面就是树的内容了,如果后面又看到了相关的题目再作补充吧,欢迎继续关注。
- 数组中的逆序对
- 数组中只出现一次的数字
- 在递增排序的数组中查找和为s的两个数字
1.数组中的逆序对
问题描述:数组中的两个数字,如果前一个数字大于后面的数字,那么这两个数组就组成一个逆序对,输入一个数组,计算数组中的逆序对的总数。例如,数组{7,5,6,4},有5对逆序对,分别是{7,6},{7,5},{7,4},{6,4},{5,4}.
算法思想
思路一:最简单的思路就是顺序遍历数组,没遍历到一个数字,就将这个数字和后面的元素依次比较,统计逆序对的数目,但是这样的思路时间耗费比较严重,每个元素都需要进行O(n)次比较,而总的元素个数是n,总时间复杂度就是O(n^2).
思路二:尝试更快的解决方法。首先,逆序对其实也就是比较大小,说到比较大小,排序我们应该很熟悉,那我们能不能在排序的过程中计算逆序对的数目呢?考虑这样一个比较过程,先将数组分为长度为2的两个子数组,然后再将子数组分为长度为1的子数组,接下来比较大小,针对第一个子数组,得到{7,5}这一对逆序对,再将长度为1的子数组合并并排序,最后将长度为2的子数组合并/排序,在这个过程中我们又可以再次计算逆序对的数目,将两个相邻子数组的最大的数进行比较7和6,前面的大数>后面的大数,逆序对数目+2,然后将较大的数7放到辅助数组中,再比较下一对,5和6进行比较,前面的数小于后面的数,没有逆序对,将较大的数放到辅助数组中,接下来比较5和4,存在逆序对,但是4是后面数组中最小的数字,所以逆序对+1,再将大的数加入辅助数组中,最后得到排好序的数组,如下图。
很明显,这是一个归并排序的过程,在归并的过程中计算逆序对的数目。归并排序的时间复杂度是O(nlog),要比顺序判断的效率高。
代码
接下来,我们就基于归并排序统计逆序对的数目。
//计算数组中逆序对的数目,返回结果
int InversePairs(int data[], int length)
{
if(data == NULL || Length <= 0)
{
fprintf(stderr, "Invalid parameter.\n");
exit(1);
}
int copy[] = new int[length];
for (int i = 0; i < length; i++)
copy[i] = data[i];
int count = InversePairsCore(data, copy, 0, length - 1);
delete copy[];
return count;
}
//用归并排序计算逆序对数目,在归并时计算逆序对,返回结果是逆序对的数目
int InversePairsCore(int data[], int *copy, int start, int end)
{
if (start == end)
return 0;//当子数组中只有一个数字时,不存在逆序对
int mid = (start + end) / 2;//start~mid是前一半子数组,mid+1~end是后一半子数组
int leftCount = InversePairsCore(data, copy, start, mid);//计算前一半子数组中逆序对的数目
int rightCount = InversePairsCore(data, copy, mid + 1, end);//计算后一半子数组中逆序对的数目
//归并
int i = mid;//定位到前一半子数组的最后一位,也就是最大的数字,从最大的数字开始比较
int j = end;//定位到后一半子数组的最大数
int indexCopy = end;//辅助数组从当前合并的数组的最高位开始更新
int count = 0;
while (i >= start && j >= (mid + 1))//从最高位往前比
{
if (data[i] > data[j])//存在逆序对
{
copy[indexCopy--] = data[i--];//将当前比较得到的较大数保存到辅助数组
count += j - mid;//当前后一半数组中小于data[i]的元素个数
}
else
copy[indexCopy--] = data[j--];
}
while (i >= start)
copy[indexCopy--] = data[i--];
while (j >= mid + 1)
copy[indexCopy--] = data[j--];
//将copy中排好序的部分复制到原数组中
for (int i = end; i > indexCopy; i--)
data[i] = copy[i];
//左半子数组中逆序对的数目+右半子数组中逆序对的数目+合并时逆序对的数目
return leftCount + rightCount + count;
}
小结
这一道题的解决思路主要就是在利用归并排序计算逆序对的数目,和归并排序相比,不过是多了计算count的部分,归并算法是关键算法。总结一下,,就当做是复习归并排序了。归并算法是一种稳定排序算法,也就是说,原数组中相等的两个数,排序之后两个数的顺序不会改变,它主要是用分治的思想,将待排数组分成2个子数组,子数组再往下分,每次分割都是一样的,从中间位置开始分,很明显这是一个递归过程,当分到子数组中只有一个数字的时候就结束了,这就是边界条件。将数组分成子数组的时间复杂度是O(logn),这个过程很像在画二叉树,而每次将子数组合并需要O(n),所以总的时间复杂度是O(nlogn),而归并排序需要一个O(n)的辅助数组来暂时存储排好序的数组,所以空间复杂度是O(n)。
2.数组中只出现一次的数字
问题描述:输入一个数组,数组中除了两个数字以外,其他数字都出现了两次,而这两个数字只出现一次,找出这两个只出现一次的数字。
算法思想
关注问题中,有的数字只出现一次,有的数字出现两次,再联系异或运算,一个数字和它本身异或结果为0,而0和任何数异或结果是任何数。假设另一种情况,假如数组中只有一个数字只出现一次,其他都出现两次,我们可以顺序异或数组中的各位,这样数组中相同的数字都抵消了,结果为0,最后的结果就是那个值出现一次的数字,abacdcb = d。
如果数组中存在两个只出现一次的数字呢,这样我们顺序异或数组中各个元素的得到的结果是这样的,abacdcb^e = d^e,很明显这不能确定最后的结果,那么如果我们将数组分为两个子数组呢,让两个只出现一次的数字分别位于两个子数组中,而其他的数字都成对出现的子数组中,这样分别对两个子数组进行异或操作就可以得到两个只出现一次的数字了,接下来的问题就是如何将原数组分开呢?
尝试利用上一次异或的结果d^e,在d!=e的情况下,异或的结果一定不会是0,这个结果的二进制表示中一定有一位是1,假设第k位是1,也就是说d和e的二进制表示下第k位一定不一样(这样才能保证异或之后结果为1),我们利用k这个数将数组分成两个子数组,每个元素二进制的第k为1的分到一个数组num1中,为0的分到数组num2中,这样能保证d和e一定能被分来,而且相同的数字的二进制表示第k位肯定是相同的,所以相同的数字会被分到一个数组中,最后在分别对两个数组依次进行异或,就可以得到最后的结果。
代码
分析清楚之后代码就简单多了,下面的代码中将原数组拆分到两个子数组操作简化了,直接进行异或操作得到结果。
int FindFirstBitIs1(int num);
bool isBit1(int num, int index);
//找到数组中只出现1次的两个数字
//num1和num2用来保存最后的结果
void FindNumsAppearOnce(int data[], int length, int *num1, int *num2)
{
if (data == NULL || length <= 0 || num1 == NULL || num2 == NULL)
{
fprintf(stderr, "Invalide parameter.\n");
exit(1);
}
//对数组中的元素依次进行异或
int result = 0;//0和任何数异或结果为任何数
for (int i = 0; i < length; i++)
result ^= data[i];
//d^e,找到这个结果的二进制表示中的第一个1
int indexOf1 = FindFirstBitIs1(result);
*num1 = *num2 = 0;//初始化为0,在拆分数组的同时进行异或运算
for (int i = 0; i < length; i++)
{
//将indexOf1为1的数组分到num1中
if (isBit1(data[i], indexOf1))
*num1 ^= data[i];
else
*num2 ^= data[i];
}
}
//找到num中的第一个1,返回下标
int FindFirstBitIs1(int num)
{
int index = 0;
//判断num最右边的一位是不是0,一个int变量的二进制表示最多有sizeof(int) * 8)位
while ((num & 1) == 0 && (sizeof(int)* 8) > index)
{
num = num >> 1;//右移一位
index++;
}
return index;
}
//判断num的二进制表示的第index位是不是1
bool isBit1(int num, int index)
{
num = num >> index;
return (num & 1);
}
小结
这一题在解决“数组中有两个只出现一次的数字”这一问题以外,还解决了“数组中只有一个只出现一次的数字”,如果问题变成,数组中除了一个数字值出现一次,其他数字都出现3次呢?
这个时候再用异或就不能解决问题了,换个角度,用位运算来解决问题,假设数组中所有的数字都出现了3次,那么这些数字的二进制各位上的1相加都是能被3整除,要么是3的倍数,要么是0,此时再对该数组添加任何一个数,如果这个数在二进制的某位上为1都将导致该位上1的个数不能被3整除,因此通过统计二进制上每位1的个数就可以推断出x在该位置上是0还是1了,这样就能计算出x了,原文链接(附代码) 。
3.在递增排序的数组中查找和为s的两个数字
问题描述:输入一个递增排序的数组和一个数字s,在数组中查找两个数,使它们的和加起来正好是s,如果有多组这样的数,任意输出一对即可。例如输入数组{1,2,4,7,11,15}和数字15,由于4+11=15,因此输出4和11.
算法思想
思路一:O(n^2)的方法,首先确定一个数字,然后遍历数组中其余(n-1)个数字,判断它们的和是不是s,这是最简单也是最快想到的方法,很明显,这一种算法的时间复杂度会很高,所以我们需要找一种更快的方法.
思路二:利用排序数组的这一特点,先选择两个数字,如果两个数字的和等于s,那么我们就找到了要找的两个数字,如果和小于s,我们希望和再大一些,可以选择较小的数字后面的数,因为数组是排好序的,排在后面的数字一定大于或等于前面的数字,就有可能找这两个数字;同样的,如果和大于s,就选择较大的数字前面的数字。
就数组{1,2,4,7,11,15}和期待数字15来说,先定义两个指针,第一个指针指向第一个数字,第二个指针指向最后一个数字,这两个指针的和是16,我们移动第二个指针,使其指向11,这时和是1+11是12,小于15,接下来移动第一个指针,指向2,这时的和是2+11是13,仍然小于15,接下来移动第一个指针,指向2,计算和是4+11=15,这样就找到了我们要找的两个数字。
这一种算法是从两边想中间扫描,在while循环内完成查找任务,所以时间复杂度是O(n).
代码
注:用64位长整型接收相加的结果,避免越界错误。
//在data数组中找到num1和num2,它们的和是sum,数组长度为length
bool FindNumbersWithSum(int data[], int length, int sum,
int* num1, int* num2)
{
bool found = false;
if (data == NULL || length <= 0 || num1 == NULL || num2 == NULL)
{
fprintf(stderr, "Invalid parameter.\n");
return found;
}
int ahead = length - 1;
int behind = 0;
while (ahead >= behind)
{
long long temp = data[ahead] + data[behind];
if (temp == sum)
{
*num1 = data[behind];
*num2 = data[ahead];
found = true;
break;
}
else if (temp < sum)
behind++;
else
ahead--;
}
return found;
}
总结
数组系列就到这里了,这一篇只写了2道题,篇幅也较小。在数组中的逆序对中,是用归并排序来解决问题,在排序同时计算逆序对的数目,而在数组中只出现一次的数字是用到了位运算的知识,这里就简单带过了,之后再补上位运算算法题的总结。最近耽搁的时间有点久,后面的算法题我会加紧跟上,欢迎继续关注~