查找算法之顺序查找

  对顺序存储的数据进行查找,最简单的算法就是从头开始,逐个检查。若能够在表中找到与给定关键字匹配的元素,则查找成功,否则查找失败。
  这个算法可以非常简单的得以实现:

typedef struct
{
        int key;
        int value;
}s_eletype;

typedef struct
{
        int size;
        s_eletype* element;
}s_list;

int search(s_list lst, int k, s_eletype* x)
{
        for (int i = 0; i < lst.size; i++)
        {
                if (lst.element[i].key == k)
                {
                        *x = lst.element[i];
                        return TRUE;
                }
        }
        return FALSE;
}

  这是一个非常普通的版本。简单,但是有没有更高效的算法呢。在与表中每一个元素的对比过程中,程序都进行了两次比较:第一次是检查数组有没有越界,即i < lst.size;第二次是检查表中对应位置上元素的关键字是否与给定的关键字匹配。假定,参数传进来的关键字一定能够在表中找到相关元素,那数组越界检查能否去掉呢,毕竟能少一步操作就少一步嘛。
  那search函数就可以改一下:

int search(s_list lst, int k, s_eletype* x)
{
        for (int i = 0; lst.element[i].key != k; i++);

        if (lst.element[i].key == k)
        {
                *x = lst.element[i];
                return TRUE;
        }
        return FALSE;
}

  这样处理确实可以在关键字确定可以得到匹配的情况下提升一些效率,但是太危险了。一旦给定的关键字在表中没有相应的元素匹配,这程序就不知道会跑哪里去,所以还需要进一步处理。为了让这个查找范围不越界,必须让它止步于表尾,怎么止步?可以利用lst.element[i].key != k这个条件,在申请表空间时可以多申请一个元素所需的空间,即size+1;在最后一个位置赋予要查找元素的关键字,这个元素又被称为哨兵。

int search(s_list lst, int k, s_eletype* x)
{
        int i = 0;
        lst.element[lst.size].key = k;

        for (; lst.element[i].key != k; i++);

        if (i < lst.size)
        {
                *x = lst.element[i];
                return TRUE;
        }
        return FALSE;
}

  显然,哨兵这种方式可以减少一次比较,但是会多消耗一个元素大小的空间。所以在选择的时候需要考虑整个表的大小,如果数据量很大,表很长,增加一个元素大小的空间来提升一定的效率是可以接受的;但是如果数据量小,减少一个比较步骤好像影响微乎其微。

  以上的实现方式并没有考虑数据的有序性,倘若数据是从小到大有序存放的呢。
例如:

key 1 2 3 5 6 7 8 9 10 哨兵
value 11 25 23 14 15 12 31 56 45

  元素关键字从小到大有序排放,如给定查找的元素关键字为3for循环只需执行三次便可查到相应元素。那如果传进来的关键字为4呢,那整个搜索过程必须找遍整个表才能结束,但是我们明明知道表中key值在5之后怎么也不可能查找到key值为4的元素,后面这些查找工作都是白做的。假如参数传进来的key在表中key[i]key[i+1]之间的概率等同,则实际需要进行比较的次数平均为(n+1)/2,但上面程序实际进行的比较次数却是(n+1)次,必须改进。
  既然是有序的,那么程序可以在lst.element[i].key >= k时退出for循环,即条件可以改为lst.element[i].key < k

int search(s_list lst, int k, s_eletype* x)
{
        int i = 0;
        lst.element[lst.size].key = k;

        for (; lst.element[i].key < k; i++);

        if (i < lst.size && lst.element[i].key == k)
        {
                *x = lst.element[i];
                return TRUE;
        }
        return FALSE;
}

  以上探讨都是为了一个目的:在每个元素等概率搜索时,减少平均搜索长度,尽可能的提高搜索的速度与效率。但是在大多数情况下,表中元素的搜索概率并不相等且无法提前计算各元素的搜索概率,或者说有的元素在一段时间内频繁地被访问,之后却很少访问。这时候就需要这个线性表有一定的自组织性。
有三种可能的自组织表方式:计数方式、移至开头和互换位置。

  • 计数方式:每个元素中都保存一个访问计数,该元素每访问一次,就将该计数值加一,当该元素的计数值大于前一个元素时,将两个元素位置互换。这种方式的缺点是每个元素需要一个计数空间,且在大量访问之后计数值的清零问题。
  • 移至开头:在成功搜索某元素之后,就将该元素移到表的开头,这种方式适合链式存储,顺序存储时需要移动大量元素,开销太大,且平均搜索长度波动会相对更大,当一个小概率元素被访问后,表的前半部分大概率元素的搜索长度都会增1。
  • 互换位置:这个方法是将成功搜索的元素与它前面的元素互换位置,这种方式可适用于链式存储和顺序存储。这种自组织方式在前期的平均搜索长度有可能会比较大,它需要一定量的访问之后才会将最常用的元素慢慢移到表的前端。

本篇完 -

你可能感兴趣的:(数据结构)