算法-数组(一)

数组的相关算法要简单一些,之前写过的和现在遇到的整理了一下。

数组:数组是较为简单的数据结构,它占据一块连续的内存,并按照顺序存储数据。数组需要事先知道容量大小,然后根据大小分配存储空间,所以数组的空间利用率不高。数组有很好的查找效率,能在O(1)内找到元素。所以我们可以基于数组实现简单的hash表,提高查找效率。

数组和指针的关系:在C语言中,我们声明一个数组时,数组名也是一个指针,数组有大小的限制,但是指针没有限制,所以在使用指针访问数组时,要注意不能越界。对数组和指针求sizeof时,有以下需要注意的地方:
1.int data[] = {0,1,2,3,4};对data求sizeof是20,因为数组中有5个整数,每个整数占4个字节。
2.int *pData = data;对pData求sizeof是4,因为pData声明为一个指针,在32位系统中,对任意指针求sizeof结果都是4。
3.void fun(int data[]){...};在函数中对data求sizeof时,结果是4,因为C语言中将数组作为参数传递时,会自动退化为指针,结果还是4.
概念就介绍这些,进入正题。

  • 二维数组中的查找
  • 旋转数组的最小数字
  • 数字在排序数组中出现的次数

1.二维数字中的查找

问题描述:每行从左到右,每列从上到下递增的二维数组中,判断某个数是否存在。

递增的二维数组

算法思想

思路一:常规思路,我们很快就能想到遍历二维数组,和要查找的数字比较,判断是否存在。这样的方法时间复杂度为O(n^2),效率很低。
思路二:我们可以利用递增的二维数组的特点,选定数组中的一个点,和要查找的key比较,如果key大于这个数,就在这个数的下面和右边查找,如果小于这个数,就在这个数的上面和左边查找,这个方法要比一个一个遍历好,但是存在一个问题,就是上面和右边方向上的查找会有重复的查找的地方。
思路三:在思路二上进行改进。我们之前的查找都是从左上角开始,那么从右上角开始呢?以上的矩阵,假如我们查找的是7,从右上角开始,先比较9和7,9比7大,9又是本列的第一个,那么可以确定,9所在的列数字都大于7,我们可以排除最后一列,前移一列。比较8和7,8大于7,同样的思想我们可以排除8所在的列。接下来比较2和7,2小于7,那么2所在的行前面的数字都小于7,可以不必再比较了,直接排除2所在的行。以此类推,我们很快就可以定位到7。

代码

思路清楚之后,代码就简单多了。
从矩阵的右上角开始进行判断,最快的排除不需要比较的列和行
1.当前元素>number,那么当前元素所在的行就可以排除-->缩小比较范围
2.当前元素<number,那么当前元素所在列可以排除
3.当前元素=number,返回true
注:传入二维数组的时候被强转成了指针,所以在取出二维数组中的数字时,要更具指针的操作规范来。

bool Find(int* matrix, int rows, int columns, int number)
{
    bool found = false;

    if (matrix != NULL && rows > 0 && columns > 0)
    {
        int row = 0;
        int column = columns - 1;
        while (row < rows && column >= 0)
        {
            if (matrix[row * columns + column] == number){//相当于matrix[row][column]
                found = true;
                break;
            }
            else if (matrix[row * columns + column] > number)
                column--;
            else
                row++;
        }
    }
    return found;
}

旋转数组的最小数字

问题描述:把数组开头的几个数字移动到数组的末尾,成为数组的旋转,输入一个递增数组的旋转,找到数组中最小的数字。

算法思想

思路一:一次遍历就可以找到最小的数字。时间复杂度为O(n).
思路二:利用递增数组的特点,利用二分法查找。我们可以发现递增数组旋转之后之后可以得到两个排序的子数组,且第一个数字会大于或者等于最后一个数字,两个排序子数组的分界点就是这个最小的数字,这样在一定程度上排序的数组,我们可以尝试使用二分法的思想。使用两个指针,一个指向数组头,一个指向数组尾,接下来可以找到中间元素,如果中间元素大于第一个元素,中间元素位于前面的子数组中,就把前一个指针移动到中间元素的位置;如果中间元素小于后一个指针指向的元素,说明中间元素位于后面的子数组中,就把后一个指针移到中间元素的位置,这样就减少了一半查找范围。当第一个指针和后一个指针相差1时,那么后一个指针所指就是最小元素了。时间复杂度为O(logn).

在数组{3,4,5,1,2}中找最小值

改进:以上思路还存在bug,我们开考虑这样的数组,如图,在第一个数组中,我们定位到中间元素后发现,前一个指针所指,中间元素和后一个指针所指都是一样,如果这个时候简单的把中间元素归到前一个子数组中,那么默认最小元素在中间元素的后面就出错了。原因在于这样的中间元素我们不能确定它是属于后面还是属于前面,这时只能顺序查找了。

数组{0,1,1,1,1}的旋转数组

代码

分析结束可以看代码了。
注意:在Min函数中,我们把indexMid初始化为index1,也就是指向第一个元素,当传入的数组是递增数组时(原数组本身也是数组的一个旋转数组),第一个元素就是最小的,这时函数会返回第一个元素。

//当index1,indexMid,index2所指元素都相等的时只能顺序查找
int MinInOrder(int *numbers, int index1, int index2)
{
    for (int i = index1 + 1; i <= index2; i++)
    {
        if (result > numbers[i])
        {
            return numbers[i];
       }

    }
    return numbers[index1];
}

//查找旋转数组中的最小数
int Min(int *numbers, int length)
{
    if (numbers == NULL || length < 0)
    {
        fprintf(stderr, "Invalid parameter\n" );
        exit(1);
    }

    int index1 = 0;
    int index2 = length - 1;
    int indexMid = index1;
    while (numbers[index1] >= numbers[index2])
    {
        if ((index2 - index1) == 1)
        {
            indexMid = index2;
            break;
        }

        indexMid = (index2 + index1) / 2;
        //特殊情况,无法判断indexMid是属于哪一个子数组
        if (numbers[index1] == numbers[indexMid] && numbers[indexMid] == numbers[index2])
            return MinInOrder(numbers, index1, index2);
        //中间元素属于前一个子数组
        if (numbers[indexMid] > numbers[index1])
            index1 = indexMid;
        else if (numbers[indexMid] < numbers[index2])
            index2 = indexMid;
    }
    return numbers[indexMid];
}

数字在排序数组中出现的次数

问题描述:统计一个数字在排序数组中出现的次数。如果数组{1,2,3,3,3,3,4,5}和数字3,3在数组中出现了4次,返回4.
这个可以看做是查找问题,关注排序数组几个字。

算法思想

思路一:最简单直接的思路就是一次遍历数组,进行统计,时间复杂度为O(n)。
思路二:常规的思路人人都能想到,虽然简单,但是考虑到数组的特点,注意,这里的数组是排序数组。说到排序数组,我们可以想到二分查找,那么如果把二分查找的思想迁移到这里呢?先找到最中间的元素,如果最中间的元素小于或者大于要统计次数的数字,那么范围减少一半,其实提高了效率,但是如果最中间的数组等于要查找的数字呢?我们就需要向两边查找,一直找到第一个数字和最后一个数字,数字在长度为n的数组中有可能出现n次,所以,时间复杂度为O(n),和思路一的时间复杂度相同了。
思路三:思路二的改进。假设要统计次数的数字为key,在思路二中,时间耗费主要是在确定第一个key和最后一个key的时候,那么如果用二分法获取第一个key和最后一个key的位置呢?先找到中间数字,如果比key小,说明第一个key在后半部分,如果比key大,说明第一个key在前半部分,如果相等的话,先看中间数字的前一个是不时也等于key,如果不等,就可以确定中间数字就是第一个key,如果相等,说明第一个key在前一半,继续查找,找最后一个key的方法也类似,可以用递归实现。这样的算法,找第一个key的时候时间复杂度为O(logn),找最后一个key的时候,时间复杂度也是O(logn),所以总的时间复杂度也是O(logn)。

代码

下面是思路三的代码,有以下需要注意的地方:

  1. 在FindFirstK和FindLastK函数中,在判断中间元素的前一个元素或后一个元素是否是key时,要先判断是否到达数组边界,防止越界。
  2. 在函数GetNumberOfK中,计算key出现的次数时先判断了firstIndex和lastIndex是否都大于-1,这是因为只要有一个值为-1,那么数组中就不可能存在这个key。因为在FindFirstK和FindLastK中,只有找不到key才会返回-1,既然数组中没有key,那么自然就不会执行middle==key下面的判断了,而这里,就是FindFirstK和FindLastK两个函数不同的地方。也就是说,当数组中没有key时,FindFirstK和FindLastK两个函数执行的过程都是一样的。所以,我们也可以改进下面的代码,在GetNumberOfK中,计算了firstIndex之后先判断其是否等于-1,如果等于-1就,不必再计算lastIndex了,直接返回0.
//递归确定第一个key的下标
int FindFirstK(int *data, int length, int start, int end, int key)
{
    if (start > end)
        return -1;

    int middleIndex = (start + end) / 2;
    int middle = data[middleIndex];

    if (middle == key)
    {
        //先判断是否大于0,再计算data[middleIndex-1]防止越界
        if ((middleIndex > 0 && data[middleIndex - 1] != key)
           || middleIndex == 0)
            return middleIndex;
        else
            end = middleIndex - 1;
    }
    else if (middle < key)
        start = middleIndex + 1;
    else//middle > key
        end = middleIndex - 1;
    FindFirstK(data, length, start, end, key);
}

//递归确定最后一个key的位置
int FindLastK(int *data, int length, int start, int end, int key)
{
    if (start > end)
        return -1;
    int middleIndex = (start + end) / 2;
    int middle = data[middleIndex];

    if (middle == key)
    {
        if ((middleIndex < length - 1 && data[middleIndex + 1] != key)
            || middleIndex == length - 1)
            return middleIndex;
        else
            start = middleIndex + 1;
    }
    else if (middle < key)
        start = middleIndex + 1;
    else//middle > key
        end = middleIndex - 1;
    FindLastK(data, length, start, end, key);
}

int GetNumberOfK(int *data, int length, int key)
{
    int count = 0;
    if (data != NULL || length > 0)
    {
        int firstIndex = FindFirstK(data, length, 0, length - 1, key);
        int lastIndex = FindLastK(data, length, 0, length - 1, key);
        if (firstIndex > -1 && lastIndex > -1)
            count = lastIndex - firstIndex + 1;
    }
    return count;
}

总结

这次的三道题说是数组相关的算法题,也可以说是和查找算法题,在二维数组中的查找中,我们利用了二维递增数组的规律,每次都找右上角的数组,便于快速排出行或者列;在旋转数组的最小数字和计算数字在排序数组中出现的次数中,我们利用了排序数组的特点,用二分法进行查找,其实是迁移了二分查找的思想,进行了变换得到的。

好了,这次就到这里了。不足之处,还请多指教。

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

推荐阅读更多精彩内容