(十二)查找和排序 -- 1. 查找

1. 查找

Pig Latin:一个模块化开发的案例研究 小节的piglatin.c中,函数FindFirstVowel的功能,是在一个字符串中查找第一个元音字母。

函数实现为:

static int FindFirstVowel(string word) {
    int i, length;
    length = StringLength(word);
    for (i = 0; i < length; i++) {
        if (IsVowel(IthChar(word, i)))
            return i;
    }
    return -1;
}

该实现采用简单直接的算法来解决查找问题,即:
从字符串的第一个字母开始,该函数依次检查每一个字母。


1.1 在整数数组中查找

可以利用以上方法在一个数组中查找某个数据项。

例如,函数FindIntegerInArray在一个有效长度为n的整数数组中查找整数key:

static int FindIntegerInArray(int key, int array[], int n) {
    int i;
    for (i = 0; i < n; i++) {
        if (array[i] == key)
            return i;
    }
    return -1;
}

使用FindIntegerInArray函数写一个程序findcoin.c来显示美国的硬币名称和它对应的值。

其中,FindIntegerInArray函数返回数组coinValues中相应硬币值对应的下标。

如果硬币是美国铸造的五种标准硬币之一,返回的下标再用来在coinNames数组中选择相应的元素。即,得到硬币的名称。

  • 并联数组(parallel array)
    用相同的下标位置来存储相关的数据称为并联数组(如下图所示)。

具体实现如下:

#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include "strlib.h"


/* Function Prototype */
static int FindIntegerInArray(int key, int array[], int n);
static int coinValues[] = {1, 5, 10, 25, 30};
static string coinNames[] = {"penny", "nickel", "dime", "quarter", "half-dollar"};


/* Main Program */
main() {
    int value, idx, nCoins;
    printf("This program looks up names of U.S. coins.\n");
    printf("Enter coin value: ");
    scanf("%d", &value);

    nCoins = sizeof coinValues / sizeof coinValues[0];
    idx = FindIntegerInArray(value, coinValues, nCoins);

    if (idx != -1) {
        printf("That's called a %s.\n", coinNames[idx]);
    } else {
        printf("There is no such coin.\n");
    }
}


/* Function */
static int FindIntegerInArray(int key, int array[], int n) {
    int i;
    for (i = 0; i < n; i++) {
        if (key == array[i])
            return i;
    }
    return -1;
}

1.2 关于查找的另一个更复杂的例子

介绍一个更复杂的查找的应用。

假设要在程序中表示一张表(如下所示),给出各城市之间的距离(用英里表示):

声明一个全局变量mileageTable,并用上表数据进行静态初始化:

#define NCities 12

static int mileage Table[NCities][NCities] = {
	{0, 1108, 708, 1430, 732, 791, 2191, 663, 854, 748, 2483, 2625}, 
	{1108, 0, 994, 1998, 799, 1830, 3017, 1520, 222, 315, 3128, 3016}, 
	{708, 994, 0, 1021, 279, 1091, 2048, 1397, 809, 785, 2173, 2052}, 
	{1430, 1998, 1021, 0, 1283, 1034, 1031, 2107, 1794, 1739, 1255, 1341}, 
	{732, 799, 279, 1283, 0, 1276, 2288, 1385, 649, 609, 2399, 2327}, 
	{791, 1830, 1091, 1034, 1276, 0, 1541, 1190, 1610, 1511, 1911, 2369}, 
	{2191, 3017, 2048, 1031, 2288, 1541, 0, 2716, 2794, 2703, 387, 1134}, 
	{663, 1520, 1397, 2107, 1385, 1190, 2716, 0, 1334, 1230, 3093, 3303}, 
	{854, 222, 809, 1794, 649, 1610, 2794, 1334, 0, 101, 2930, 2841}, 
	{748, 315, 785, 1739, 609, 1511, 2703, 1230, 101, 0, 2902, 2816}, 
	{2483, 3128, 2173, 1255, 2399, 1911, 387, 3093, 2930, 2902, 0, 810}, 
	{2625, 3016, 2052, 1341, 2327, 2369, 1134, 3303, 2841, 2816, 810, 0}, 
};

对应的城市名称可以被存储在一个一维数组cityTable中,该数组的声明和初始化过程如下:

static string cityTable[NCities] = {
	"Atlanta",
	"Boston",
	"Chicago",
	"Denver",
	"Detroit",
	"Houston",
	"LosAngeles",
	"Miami",
	"NewYork",
	"Philadelphia",
	"San Francisco",
	"Seattle",
};

如何写一个程序读入两个城市的名字,显示它们之间的距离,运行结果如下所示:

步骤如下:
(1) 以字符串的形式读入两个城市的名字。
(2) 在数组cityTable中找到该城市名字所对应的下标位置。
(3) 用下标值在数组mileageTable中找到对应的值,这个值就是两城市之间的距离。

假设能够实现一个在字符串数组中查找的函数FindStringInArray,那么,程序的其余部分可以写成:

main() {
    int city1, city2;

    printf("This program looks up intercity mileage.\n");

    city1 = GetCity("Enter name of city #1: ");
    city2 = GetCity("Enter name of city #2: ");

    printf("Distance between %s ", cityTable[city1]);
    printf("and %s: ", cityTable[city2]);
    printf("%d miles.\n", mileageTable[city1][city2]);
}


static int GetCity(string prompt) {
    int idx;
    char tmp;
    string cityName;
    cityName = malloc(100);

    while (true) {
        printf("%s", prompt);

        // Inputting blank space is supported, but \n isn't.
        scanf("%[^\n]", &cityName[0]);
        scanf("%c", &tmp);
        
        idx = FindStringInArray(cityName, cityTable, NCities);
        if (idx >= 0) break;
        
        printf("Unkown city name -- try again.\n");
        
    }
    return idx;
}

1.3 线性查找

如果遵循实现FindIntegerInArray的逻辑,只需要改变参数类型,在循环中使用StringEqual比较字符串的值。相应的代码如下:

static int FindStringInArray(string key, string array[], int n) {
    int i;
    // printf("key: %s\n", key);
    for (i = 0; i < n; i++) {
        // printf("array %d: %s\n", i, array[i]);
        if (StringEqual(key, array[i]))
            return i;
    }
    return -1;
}
  • 线性查找算法(linear search algorithm)
    搜索将从数组的第一个元素开始按顺序查找,直到发现匹配的元素为止,否则将在达到数组的最后一个元素时终止。

函数FindStringInArray(和前文的FindIntegerInArray)中使用的算法均为线性查找。


现实中,是否会从表头开始,按顺序找下去呢?

很可能不会。因为表中所列的城市名是按照字母顺序排列的。

San Francisco一定接近表的尾部,而Boston一定接近表的头部。

这种差异使得能很快找到这个值,而不需要检查整张表的大部分城市名。


1.4 二分查找

为了利用数组cityTable已经按照字母顺序排列这个条件,需要采用一个新的算法。

假定所要查找的是下图所示数组中的San Francisco:

如果不再采用从数组第一个元素开始的线性搜索方法,而从数组的接近中间的位置取一个值,中间元素的下标值可以通过计算下标范围的两个端点的均值得到,即:
0 + 11 2 \frac{0+11}{2} 20+11

当使用整数进行运算时,该表达式的值为5。

存储在cityArray[5] 中的城市名是Houston。由于San Francisco所在位置一定在Houston的后面,因此可以立即排除下标值从0~5的城市名。

下面的查找中,可以采用相同的方法:

由此,整个搜索过程只需要三步。


  • 二分查找(binary search)
    首先在已排序的数组中查找中间元素,然后根据这个元素的值确定下一步将在哪个部分进行搜索,这种查找方法称为二分查找。

为了实现这种算法,需要记录两个下标值,即,表示要进行搜索的范围的两个端点的下标值。

在函数中,这两个下标值分别存储于变量lhrh中,表示左边界(小的下标值)和右边界(大的下标值)。

使用二分查找的函数FindStringInSortedArray的代码如下:

static int FindStringInSortedArray(string key, string array[], int n) {
    int lh = 0;
    int rh = n - 1;
    int mid, val;
    while (true) {
        mid = (lh + rh) / 2;
        val = StringCompare(key, array[mid]);
        if (val == 0) {
            return mid;
        } if (val < 0) {
            rh = mid;
        } else {
            lh = mid;
        }
    }
    return -1;
}

1.5 查找算法的相对效率

对于查找算法来说,一个能对算法性能进行很好评估的比较方便的方法,就是计算调用StringEqualStringCompare的次数,每次调用都是比较键值和数组中的某些元素。

假设在一个长度为 N N N的数组中用线性查找算法进行搜索,在最坏的情况下,FindStringInArray函数将调用 N N NStringEqual函数,即,对数组中的每一个元素都要调用一次。

在函数FindStringInSortedArray实现中所用的二分查找算法,每经过一次调用,可能的元素数都减半,最终搜索区间将变为1。

达到这一点所需的步数等于将 N N N依次除以2并最终得到1所需要的次数,该过程可用如下公式表示:

将所有的2相乘可得以下方程:
N = 2 k N = 2^k N=2k

取对数可得k值:
k = l o g 2 N k = log_2N k=log2N

因此,使用线性查找算法查找由 N N N个元素组成的数组需要 N N N次的比较,而使用二分查找算法只需要 l o g 2 N log_2N log2N次比较就可以了。

二分查找算法唯一的问题是,数组必须是严格地按照某个顺序排列的。
如果不是,则必须采用线性查找算法。或者自己先把该数组按要求进行排序。





参考
《C语言的科学和艺术》 —— 第12章 查找和排序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值