算法-数组(四)

数组系列文章也到尾声了,这是数组系列的最后一篇文章,后面就是树的内容了,如果后面又看到了相关的题目再作补充吧,欢迎继续关注。

  • 数组中的逆序对
  • 数组中只出现一次的数字
  • 在递增排序的数组中查找和为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道题,篇幅也较小。在数组中的逆序对中,是用归并排序来解决问题,在排序同时计算逆序对的数目,而在数组中只出现一次的数字是用到了位运算的知识,这里就简单带过了,之后再补上位运算算法题的总结。最近耽搁的时间有点久,后面的算法题我会加紧跟上,欢迎继续关注~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 一. 写在前面 要学习算法,“排序”是一个回避不了的重要话题,在分析完并查集算法和常用数据结构之后,今天我们终于可...
    Leesper阅读 2,463评论 0 40
  • 该系列文章主要是记录下自己暑假这段时间的学习笔记,暑期也在实习,抽空学了很多,每个方面的知识我都会另起一篇博客去记...
    Yanci516阅读 12,067评论 6 19
  • 四. 走向世界之巅——快速排序 你可能会以为归并排序是最强的算法了,其实不然。回想一下,归并的时间效率虽然高,但空...
    Leesper阅读 1,467评论 9 7
  • 思学《论语》1.14 宜山一中 薛思雪 【原文】 1•14 子曰:“君子食无求饱,居无求安,敏于事...
    思雪_2988阅读 933评论 1 3
  • 这是“心灵对话.写作"小组第17篇文章 早晨,边听广播边吃饭,当听到主播说:好多人都说,年轻时一定要出名,否...
    落字生香阅读 290评论 0 2