针对范围对的高效查找算法设计(不准用数组)

题目链接在:针对一群范围对的最快查找算法设计(不要用数组),是我目前遇到的一个较棘手的问题。

描述如下:

假如有一群范围对,格式为:<范围表示,该范围对应的结果值>,设计一个最快查找算法,使得给定一个值,输出该值所在范围对的结果值。

注意:范围对之间没有交集,即不可能存在<1, 10>和<2, 11>这样的两个范围对。

例如有以下几个范围对:

<<1, 2>, 20>

<<3, 37>, 27>

<<48, 57>, 28>

<<58, 63>, 27>

<<97, 128>, 122>

<<129, 149>, 12>

<<150, 189>, 13>

<<200, 245>, 14>

<<246, 256>, 129>

<<479, 560>, 12>

假如给定一个数100,则根据题意应输出122,因为100属于范围对<97, 128>

要求:不要用范围对作为下标用数组来存储,因为范围对可能非常大。

对于这个问题,思考许久,有了下面几个思路

1. 用STL map来存储这些范围对(key)及对应的结果集(value),用map进行查找

范围对定义如下:

class range {
public:
    int from;
    int to; 

public:
    range(): from(-1), to(-1) {}
    range(int f, int t): from(f), to(t) {}
};

map定义为:

typedef map<range*, int> range_map;

但这里有个问题,map的key是自定义类型,一般需要自定义比较函数才能进行查找,一般的自定义比较函数如下:

struct cmp_func {
    bool operator()(const range* lc,const range* rc) const {
        return (lc->from < rc->from) || (lc->from == rc->from && lc->to < rc->to);
    }
};

但这样的比较函数并不适用于我们的需求,因为我们要求查询的并不是一个范围对,即并不是查询map中有没有<3, 37>这样的范围对,而是要求给定一个值,查询这个值属于哪个范围对,那么能不能自定义一个这样的比较函数呢?以上面那个例子为例,如果我们查找35这个数,我们将35包装成一个范围对<35, 35>,然后查找它包含在map中的哪个范围对,上面的例子是包含在<3, 37>这样的范围对,这样就找到了,也就是两个key相等,只要它们包含在同一个范围对即可。这似乎有点奇怪,违背了通常意义上的比较含义(也就是两个key相等,两个key的组成部分都应该相同才是)。不管如何,这样的比较函数还是比较简单的,如下:

struct cmp_func {
    bool operator()(const range* lc,const range* rc) const {
        return lc->to < rc->from;
    }
};

这样就实现了我们用map的find函数来查找给定的一个数属于哪个范围对了。当然,这时我们的map定义就变成了:

typedef map<range*, int, cmp_func> range_map;

用map查找表面上看上去应该挺高效的,至少比一个个顺序查找要快吧,但事实却并非如此。我用未自定义比较函数的map顺序查找和自定义上面比较函数的map find查找,结果却发现用自定义比较函数后的效果并不好,竟然比顺序查找还要慢,下面的粗糙的测试程序:

#include<iostream>
#include<stdio.h>
#include<map>
#include<sys/time.h>
using namespace std;

class range {
public:
    int from;
    int to; 

public:
    range(): from(-1), to(-1) {}
    range(int f, int t): from(f), to(t) {}
};

struct cmp_func {
    bool operator()(const range* lc,const range* rc) const {
        return lc->to < rc->from;
    }
};

typedef map<range*, int, cmp_func> range_map;

int get_next1(range_map *rm, int c) {
    for(range_map::iterator it = rm->begin(); it != rm->end(); ++it) {
        if(c >= it->first->from && c <= it->first->to) return it->second;
    }

    return -1; // not found.
}

int get_next2(range_map *rm, int c) {
    range_map::iterator iter = rm->find(new range(c, c));
    if(iter != rm->end())  return iter->second;

    return -1;  // not found.
}

int main()
{
    struct timeval t_begin, t_end;

    range_map *rm = new range_map();

    rm->insert(pair<range*, int>(new range(1, 2), 20));
    rm->insert(pair<range*, int>(new range(3, 37), 27));
    rm->insert(pair<range*, int>(new range(48, 57), 28));
    rm->insert(pair<range*, int>(new range(58, 63), 27));
    rm->insert(pair<range*, int>(new range(97, 128), 122));
    rm->insert(pair<range*, int>(new range(129, 149), 12));
    rm->insert(pair<range*, int>(new range(150, 189), 12));
    rm->insert(pair<range*, int>(new range(200, 245), 14));
    rm->insert(pair<range*, int>(new range(246, 256), 129));
    rm->insert(pair<range*, int>(new range(479, 560), 12));

    gettimeofday(&t_begin,NULL);
    int result[256];
    for(int c = 0; c < 256; c++)
        result[c] = get_next1(rm, c);
    gettimeofday(&t_end,NULL);
    double timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec);
    timeuse/=1000000;
    printf("\nget_next1 time use: %.12f\n", timeuse);
    for(int c = 0; c < 256; c++)
        cout << result[c] << " ";
    cout << endl;

    gettimeofday(&t_begin,NULL);
    for(int c = 0; c < 256; c++)
        result[c] = get_next2(rm, c);
    gettimeofday(&t_end,NULL);
    timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec);
    timeuse/=1000000;
    printf("\nget_next2 time use: %.12f\n", timeuse);
    for(int c = 0; c < 256; c++)
        cout << result[c] << " ";
    cout << endl;

    return 0;
}

运行结果为:

get_next1 time use: 0.000124000000
get_next2 time use: 0.000144000000

当然这个例子并不能代表所有情况,且每次运行结果也不一样,但从每次的运行结果来看,几乎没有一次是用自定义比较函数比顺序查找情况好的。这至少说明了一点:我们的自定义比较函数让map在查找时做了一些额外的工作,减慢了速度。比如我们为了使用map的find函数,不得不封装我们的一个数为一个range对象,在查找的时候还得调用我们自定义的比较函数进行处理。

难道就只能顺序查找吗?在这个不靠谱的思路过后又萌生了另一个不靠谱的思路。

2. 使用二分查找的思想来查找范围对

我们使用ranges和results这两个数组来保存范围对及对应的结果,按序保存,每两个ranges数对应一个results里的数。

例如上面的例子保存为:

int ranges[] = {1, 2, 3, 37, 48, 57, 58, 63, 97, 128, 129, 149, 150, 189, 200, 245, 246, 256, 479, 560};

int results[] = {20, 27, 28, 27, 122, 12, 13, 14, 129, 12};

使用二分查找来查找某个数属于哪个范围对。那么如何查找呢?比如查找35属于哪个范围对,首先与最中间的128进行比较,35<128,这时候有两种可能:

(1)100在128前半部分的数组里,即1, 2, 3, 37, 48, 57, 58, 63, 97;

(2)由于128是范围对<97, 128>的第二部分,那么也有可能这个数属于这个范围对。

由于35不属于这个范围对,那么只有在97之前的部分找(不包括97),继续二分即与37进行比较,35 < 37,与上类似,此时35属于范围对<3, 37>,也就是找到了。

再举个例子,找130属于哪个范围对,同样的先与128比较,130 > 128,这时候130只可能在128的后半部分而不需要判断是否属于范围对<128, 129>,因为<128, 129>不是范围对。怎么判断是不是范围对呢?很简单,根据当前位置的奇偶性判断即可。

下面是我写的二分查找算法,及与map顺序查找、数组顺序查找的简单对比试验:

#include<iostream>
#include<stdio.h>
#include<map>
#include<sys/time.h>
using namespace std;

class range {
public:
    int from;
    int to; 

public:
    range(): from(-1), to(-1) {}
    range(int f, int t): from(f), to(t) {}
};

typedef map<range*, int> range_map;

int get_next1(range_map *rm, int c) {
    for(range_map::iterator it = rm->begin(); it != rm->end(); ++it) {
        if(c >= it->first->from && c <= it->first->to) return it->second;
    }

    return -1; // not found.
}
// binary search
int get_next2(int *ranges, int *results, int size, int c) {
    if(size <= 1) return -1;

    int start, end, mid;
    start = 0;
    end = size - 1;

    while(start <= end) {
        if(c < ranges[start] || c > ranges[end]) return -1;
        mid = start + (end - start) / 2;
        if(c == ranges[mid]) return results[mid / 2];
        if(c < ranges[mid]) {
            if(mid % 2 == 1) {
                if(c >= ranges[mid - 1]) return results[mid / 2];
                else end = mid - 2;
            }
            else end = mid - 1;
        }
        else {
            if(mid % 2 == 0) {
                if(c <= ranges[mid + 1]) return results[mid / 2];
                else start = mid + 2;
            }
            else start = mid + 1;
        }
    }

    return -1;  // not found.
}

int get_next3(int *ranges, int *results, int size, int c) {
    for(int i = 0; i < size;) {
        if(i % 2 == 0) {
            if(c >= ranges[i] && c <= ranges[i + 1]) return results[i / 2];
            else if(c < ranges[i]) return -1;
            else i += 2;
        }
    }
}

int main()
{
    struct timeval t_begin, t_end;

    range_map *rm = new range_map();

    rm->insert(pair<range*, int>(new range(1, 2), 20));
    rm->insert(pair<range*, int>(new range(3, 37), 27));
    rm->insert(pair<range*, int>(new range(48, 57), 28));
    rm->insert(pair<range*, int>(new range(58, 63), 27));
    rm->insert(pair<range*, int>(new range(97, 128), 122));
    rm->insert(pair<range*, int>(new range(129, 149), 12));
    rm->insert(pair<range*, int>(new range(150, 189), 13));
    rm->insert(pair<range*, int>(new range(200, 245), 14));
    rm->insert(pair<range*, int>(new range(246, 256), 129));
    rm->insert(pair<range*, int>(new range(479, 560), 12));

    int ranges[] = {1, 2, 3, 37, 48, 57, 58, 63, 97, 128, 129, 149, 150, 189, 200, 245, 246, 256, 479, 560};
    int results[] = {20, 27, 28, 27, 122, 12, 13, 14, 129, 12};

    // int r = get_next2(ranges, results, 20, 65);
    // cout << r << endl;

    gettimeofday(&t_begin,NULL);
    int result[256];
    for(int c = 0; c < 256; c++)
        result[c] = get_next1(rm, c);
    gettimeofday(&t_end,NULL);
    double timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec);
    timeuse/=1000000;
    printf("\nget_next1 time use: %.12f\n", timeuse);
    // for(int c = 0; c < 256; c++)
    //     cout << result[c] << " ";
    // cout << endl;

    gettimeofday(&t_begin,NULL);
    for(int c = 0; c < 256; c++)
        result[c] = get_next2(ranges, results, 20, c);
    gettimeofday(&t_end,NULL);
    timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec);
    timeuse/=1000000;
    printf("\nget_next2 time use: %.12f\n", timeuse);
    // for(int c = 0; c < 256; c++)
    //     cout << result[c] << " ";
    // cout << endl;

    gettimeofday(&t_begin,NULL);
    for(int c = 0; c < 256; c++)
        result[c] = get_next3(ranges, results, 20, c);
    gettimeofday(&t_end,NULL);
    timeuse=1000000*(t_end.tv_sec-t_begin.tv_sec)+(t_end.tv_usec-t_begin.tv_usec);
    timeuse/=1000000;
    printf("\nget_next3 time use: %.12f\n", timeuse);
    // for(int c = 0; c < 256; c++)
    //     cout << result[c] << " ";
    // cout << endl;

    return 0;
}

运行结果为:

get_next1 time use: 0.000302000000
get_next2 time use: 0.000043000000
get_next3 time use: 0.000165000000

说明二分查找算法还是挺高效的,顺序查找也不错,有时候表现的与二分查找差不多,这里的数据比较少,体现不出准确的对比,但至少可能说明二分查找算法比简单的顺序查找(map顺序和数组顺序查找)要快不少。

上面是自己的一点拙见,相信二分查找算法肯定不是最高效的算法,但目前实在想不出更好的办法了。大家有想法的尽管提,不试试不知道算法好不好!

针对范围对的高效查找算法设计(不准用数组),布布扣,bubuko.com

时间: 2024-10-25 19:58:58

针对范围对的高效查找算法设计(不准用数组)的相关文章

AACOS:基于编译器和操作系统内核的算法设计与实现

AACOS:基于编译器和操作系统内核的算法设计与实现 [计算机科学技术] 谢晓啸 湖北省沙市中学 [关键词]: 编译原理,操作系统内核实现,算法与数据结构,算法优化 0.索引 1.引论 1.1研究内容 1.2研究目的 1.3研究提要 正文 2.1研究方法 2.2编译器部分 2.2.1从计算器程序中得到的编译器制作启示 2.2.2在编译器中其它具体代码的实现 2.2.3编译器中栈的高级应用 2.2.3编译器中树的高级应用 2.2.4编译器与有限状态机 2.3操作系统内核部分 2.3.1操作系统与底

二分查找算法(递归与非递归两种方式)

首先说说二分查找法. 二分查找法是对一组有序的数字中进行查找,传递相应的数据,进行比较查找到与原数据相同的数据,查找到了返回1,失败返回对应的数组下标. 采用非递归方式完成二分查找法.java代码如下所示. /* * 非递归二分查找算法 * 参数:整型数组,需要比较的数. */ public static int binarySearch(Integer[]srcArray,int des){ //第一个位置. int low=0; //最高位置.数组长度-1,因为下标是从0开始的. int h

Java 查找算法

这个问题有几个点要先确认 必须是有序,如果无序的话就只能全遍历了 查找算法跟数据结构相关,不同的数据结构适用于不同的查找算法 查找算法与磁盘I/O有一定的关系,比如数据库在索引排序的时候,如果每次都从磁盘读取一个节点然后进行判断 数组 如果知道下标的话就方便了,查找的复杂度为1. 如果是针对值的查找,那么顺序遍历是O(n), 二分查找 使用二分查找的话可以减少时间复杂度为:O(logn) /** * 二分查找又称折半查找,它是一种效率较高的查找方法. [二分查找要求]:1.必须采用顺序存储结构

二分查找算法java

二分查找又称折半查找,它是一种效率较高的查找方法. 折半查找的算法思想是将数列按有序化(递增或递减)排列,查找过程中采用跳跃式方式查找,即先以有序数列的中点位置为比较对象,如果要找的元素值小于该中点元素,则将待查序列缩小为左半部分,否则为右半部分.通过一次比较,将查找区间缩小一半. 折半查找是一种高效的查找方法.它可以明显减少比较次数,提高查找效率.但是,折半查找的先决条件是查找表中的数据元素必须有序. 折半查找法的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除

Java二分查找算法

二分查找又称折半查找,它是一种效率较高的查找方法. 折半查找的算法思想是将数列按有序化(递增或递减)排列,查找过程中采用跳跃式方式查找,即先以有序数列的中点位置为比较对象,如果要找的元素值小于该中点元素,则将待查序列缩小为左半部分,否则为右半部分.通过一次比较,将查找区间缩小一半. 折半查找是一种高效的查找方法.它可以明显减少比较次数,提高查找效率.但是,折半查找的先决条件是查找表中的数据元素必须有序. 折半查找法的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除

算法_001_二分查找算法

 二分查找算法是在有序数组中用到的较为频繁的一种算法,在未接触二分查找算法时,最通用的一种做法是,对数组进行遍历,跟每个元素进行比较,其时间为O(n).但二分查找算法则更优,因为其查找时间为O(lgn),譬如数组{1, 2, 3, 4, 5, 6, 7, 8, 9},查找元素6,用二分查找的算法执行的话,其顺序为:     1.第一步查找中间元素,即5,由于5<6,则6必然在5之后的数组元素中,那么就在{6, 7, 8, 9}中查找,    2.寻找{6, 7, 8, 9}的中位数,为7,7>

算法----二分查找算法

二分查找算法是在有序数组中用到的较为频繁的一种算法,在未接触二分查找算法时,最通用的一种做法是,对数组进行遍历,跟每个元素进行比较,其时间为O(n).但二分查找算法则更优,因为其查找时间为O(lgn),譬如数组{1, 2, 3, 4, 5, 6, 7, 8, 9},查找元素6,用二分查找的算法执行的话,其顺序为: 1.第一步查找中间元素,即5,由于5<6,则6必然在5之后的数组元素中,那么就在{6, 7, 8, 9}中查找, 2.寻找{6, 7, 8, 9}的中位数,为7,7>6,则6应该在7

二分查找算法(递归,循环)

二分查找算法是在有序数组中用到的较为频繁的一种算法,在未接触二分查找算法时,最通用的一种做法是,对数组进行遍历,跟每个元素进行比较,其时间为O(n).但二分查找算法则更优,因为其查找时间为O(lgn),譬如数组{1, 2, 3, 4, 5, 6, 7, 8, 9},查找元素6,用二分查找的算法执行的话,其顺序为:    1.第一步查找中间元素,即5,由于5<6,则6必然在5之后的数组元素中,那么就在{6, 7, 8, 9}中查找,    2.寻找{6, 7, 8, 9}的中位数,为7,7>6,

在一个整型数组中有一个元素的出现次数超过了数组长度的一半,试设计一个 在时间上尽可能高效的算法,找出这个元素。

题目:在一个整型数组中有一个元素的出现次数超过了数组长度的一半,试设计一个 在时间上尽可能高效的算法,找出这个元素.要求:(1)给出算法的基本设计思想.(2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释.(3)说明你所设计算法的时间复杂度和空间复杂度. (1)基本的设计思想: 一个数字出现的次数超过了长度的一半, 那么我们可以这样认为这个数字出现的个数一定大于其他全部数字出现的个数之和.算法的步骤如下: ①设数组为data[],数组长度为n,i=1.置currentAxi