算法新解前言:最小可用id求解

算法新解前言:最小可用id求解

前言

这是我第一次在csdn上写博客,主要是打算记录一下自己学习算法的一些过程。

遥想大一当年学姐建议我去学习一下算法,时光匆匆,四年转眼即逝,已经研一了,发现这四年确实走了不少弯路。最近发生了几件事,让我萌生了写算法博客的想法。

一是这学期有一门算法课,汤老师虽然讲得很快,不过思路确实讲得好,但是光听课不实践,学习效果就会差很多;
二是有一个学弟学习算法遇到了瓶颈,考PAT总是不顺利,让我想起大二时的自己,那时也是一脸茫然,没有人指引,算法学习之路确实困难;
三是最近打算写了一些算法题,发现自己的算法基础倒退得有点厉害,因此利用写博客的机会理清算法思路。

最重要的一点,就是为了改变自己在本科时期的缺点:做一件事不够坚持,我希望自己能在平常做项目、写文档的同时,依然能坚持学习算法,体会到算法的乐趣。

准备工作

机器:小米笔记本pro15.6,16GB内存+256GB SSD/第八代i7处理器
IDE: Clion
语言:主要为C++11
学习思路:算法思路最重要,同时也不应忽视实现细节
推荐书籍:数据结构与算法分析:C语言描述 + 算法第四版
在这里插入图片描述

在这里插入图片描述
当然算法的经典书籍很多,不过这两本打基础绝对是够了。

题目:最小可用id

介绍:

在这里插入图片描述

1、暴力法

书本中提到的第一个解法,即题目中伪代码描述的一样:
(1)从0开始,检查当前id是否已被使用,是则查询下一个id,不是则返回当前id;
(2)每一次查询当前id的状态,采用的方法是遍历已使用的id列表;
(3)该解法的时间复杂度为o(n^2)

暴力法虽然效率低,但是算法求解首先要解决的问题是正确性问题,其次再考虑效率问题,因此我们应该尽快用最直观的方法来获取初始版本的算法,方便验证之后算法的正确性。

2、用空间换时间法

书本中提到的第二个解法。
一般来说,如果想要提高算法的效率,就需要仔细分析问题,
利用问题的特性进行求解,那么这个问题有哪些特性呢?
(1)id从0开始
(2)寻找的是最小可用id
=> 得出结论
(3)已用id列表为A,列表的大小|A|=n,则
			minid <= n
此结论简单用反证法即可证明,此处不赘述。
因为最小可用id不大于n,因此我们仅需使用一个长度为n+1的列表,
记录不大于n的id已使用时
伪代码如下:

在这里插入图片描述

在没有想出更高效方法的情况下,可以使用空间换时间的方法。

3、先排序后查找

我想到的第一个算法,利用问题寻找最小元素的特性。
看到“最小”,第一反应是一定会运用到排序的思想。
我们观察这样一组数据:
{0,1,2,3,4,6,7,9,10}
不难发现最小可用id(unused_min_id)是5,更关键的是以下性质:
(1)array[i] == i , i < unused_min_id;
(2)array[i] != i , i >= unused_min_id;
  =>
(3)如果整个列表都符合性质(1),则最小可用id是len(array) + 1,
	否则最小可用id就是已用id列表中第一个符合性质(2)的元素下标,

代码如下:

int sort_search(int* array, int array_size){
    //先排序

    sort(array,array + array_size);

    //后查找

    for (int i = 0; i < array_size; ++i) {
        if(array[i] != i){
            return i;
        }
    }

    return array_size;
}

不难得出,时间复杂度为o(nlogn)。

4、分治法求解

我想到第二个解法,是受上一个解法启发的。
先排序后查找,这不是和第K小数的普通求解方法一致吗?

讲到第k小数,就不能不讲到快排过程。
快排的核心思想:
(1)在一次快排交换过程中选出一个标杆值,在此过程中标杆值不变。
(2)通过元素与标杆值比较,找出位置靠前的大数和位置靠后的小数,进行交换。
(3)全部元素扫描完则交换结束,将标杆值放在对应位置,
形成以标杆值下标为界,左区间元素均不大于右区间元素的左小右大局面。
(4)分而治之,继续在左区间、右区间递归地执行排序。
(5)递归终止条件:区间内元素无需排序(没有或只有一个元素)

4.1快排介绍

快排方法很多,简单起见,选择最简单的单向扫描版快排如下:

void quick_sort(int* array,int left,int right){
    //递归终止条件:没有元素或者只有一个元素,无需进行排序

    if(left + 1>= right){
        return;
    }
    //选出标杆值,交换过程中不移动

    int benchmark = array[left];
    //已排序的最小id

    int mined_id = left;

    //交换过程
    
    for(int cur_id = mined_id + 1;cur_id < right;cur_id++){
        if(array[cur_id] <= benchmark){
            //下一个id为安放小数位置

            mined_id++;
            swap(array[mined_id],array[cur_id]);
        }
    }
    //最后移动标杆值,保证左小右大
    swap(array[left],array[mined_id]);

    //继续排序
    quick_sort(array,left,mined_id);
    quick_sort(array,mined_id+1,right);
}

这个版本很好记,一个固定标杆值benchmark = array[left],
[left + 1,mined_id]区间存储的是小数,
[mined_id + 1,cur_id)存储的是大数,
[cur_id +1,right)存储的是未扫描的数。
当前扫描的元素不是大数时,则扩充小数区间大小(mined_id++),
并将当前第一个“大数”与当前扫描元素进行交换(swap(array[mined_id],array[cur_id]))。
扫描结束后,再将标杆值放在分界点即可(swap(array[left],array[mined_id]))。

4.2第k小数介绍

第k小数问题定义:在一个无序列表中找到第k小的元素。
先排序后查找解法的时间复杂度为o(nlogn),
此算法不够高效的原因很简单:多做了事情,我们只需要找出哪一个元素是第k小即可,
并不需要找出所有元素的大小关系。

那么我们快速找到元素方法是什么呢,我们仔细分析下快排过程。
不管排序算法具体过程是什么,它们的每一次子过程的调用都只为了做好一件小事。
(1)归并排序的子过程,注重于分治法的合并过程,即:
     如何将两个有序的列表进行合并。
(2)快速排序的子过程,注重于分治法的划分过程,即:
	 如何划分,使得划分的(子过程)子任务区间,维持左小右大特性。

为了实现任务的划分,每次快排过程都必须确定标杆值在列表位置,
因此我们可以使用快排过程来查找第k小数。

快排过程+二分查找思想,不断缩小求解规模,
代码如下:

int search_k_min(int* array,int left, int right ,int k) {
    while(1) {
        //一次快排过程

        int benchmark = array[left];
        int mid_id = left;

        for (int cur_id = mid_id + 1; cur_id < right; cur_id++) {
            if (array[cur_id] <= benchmark) {
                mid_id++;
                swap(array[mid_id], array[cur_id]);
            }
        }
        swap(array[left], array[mid_id]);

        //确定标杆值位置mid_id与k之间的关系
        //相等则返回对应值
        // 第k小数在右区间,移动区间左端点
        // 第k小数在左区间,移动区间左端点

        if (mid_id == k) {

            return array[k];
        } else if (mid_id < k) {

            left = mid_id + 1;
        } else {

            right = mid_id;
        }
        //如果k不在区间范围内,一种异常处理方案

        if (left >= right) {
            if (k < left) {
                return array[left];
            } else {
                return array[right - 1];
            }
        }
    }
}

4.3 最小可用id快速求解

利用在第3小节中发现的性质

(1)array[i] == i , i < unused_min_id;
(2)array[i] != i , i >= unused_min_id;
=>
(3)如果整个列表都符合性质(1),则最小可用id是len(array) + 1,
否则最小可用id就是已用id列表中第一个符合性质(2)的元素下标。

将寻找最小可用id问题转化为:
寻找已用id列表中,符合性质(1)元素的左闭右开区间的右端点。
这就和按顺序坐座位一样,前面的同学都正确坐好位置,
到第right个位置上坐的却不是第right个同学,那他一定是第一个可调整的位置。

代码如下,只要根据相关性质,对4.3中代码进行适当调整即可:

int search_unused_min_id_loop(int *array, int left, int right) {

    while (left < right) {
        //一次快排过程

        int benchmark = array[left];
        int mid_id = left;

        for (int cur_id = mid_id + 1; cur_id < right; ++cur_id) {
            if (array[cur_id] <= benchmark) {
                mid_id++;
                swap(array[cur_id], array[mid_id]);
            }
        }
        swap(array[left], array[mid_id]);

        //找到中间标杆位置后,分情况继续搜寻

        if (array[mid_id] == mid_id) {
            //左侧至mid_id全部已使用,则往右侧搜寻

            left = mid_id + 1;
        } else {
            //左侧至mid_id,有id未使用,则往左侧搜寻

            right = mid_id;
        }
    }


    return right;
}

时间复杂度为o(n),且无需额外的空间消耗。

5、算法性能测试

5.1 测试数据生成

从0开始,按顺序存放101000个数据,然后洗牌(两两交换)5000次,接下来丢掉1000个数据,最后存储到对应的文件中。

	//设立随机种子

    srand((unsigned  int)time(NULL));

    //存储丢弃数据标记

    unordered_set<int > drop_set;

    int drop_size = 1000;
    int max_limit = 101000;

    //生成数据并随机打乱

    int* id_list = new int[max_limit];
    for (int j = 0; j < max_limit; ++j) {
        id_list[j] = j;
    }

    int shuffle_size = 5000;
    for (int k = 0; k < shuffle_size; ++k) {
        int left = rand()%max_limit;
        int right = rand()%max_limit;

        swap(id_list[left],id_list[right]);
    }

    //生成丢弃id

    for (int i = 0; i < drop_size; ) {
        int id = rand() % max_limit;
        if(drop_set.end() == drop_set.find(id)){
            drop_set.insert(id);
            i++;
        }
    }

    //存储洗牌后未丢弃数据到文件中

    ofstream out_txt("./id.txt",ofstream::out);

    //数据个数
    
    out_txt<<max_limit - drop_size<<endl;

    for (int j = 0; j < max_limit; ++j) {
        //要丢弃的数据下标,则跳过

        if(drop_set.end() != drop_set.find(j)){
            continue;
        }
        out_txt<<id_list[j]<<" ";
    }
    out_txt.close();

    delete[] id_list;

5.2 算法3测试

    cout << "start" << endl;
    start = clock();
    int unused_id = sort_search(array,0,a_size);
    end = clock();
    cout << "result:" << unused_id << endl;
    cout << "cost time:" << (double) (end - start) / CLOCKS_PER_SEC << " s" << endl;

测试结果:
在这里插入图片描述

5.4 算法4性能测试

	cout << "start" << endl;
    start = clock();
    unused_id = search_unused_min_id_loop(array, 0, a_size);
    end = clock();
    cout << "result:" << unused_id << endl;
    cout << "cost time:" << (double) (end - start) / CLOCKS_PER_SEC << " s" << endl;

测试结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qiu924313181/article/details/82820720