Bootstrap

【算法篇C++实现】常见查找算法


🚀一、预备知识

⛳(一)查找的定义

查找: 又称检索或查询,是指在查找表中找出满足一定条件的结点或记录对应的操作。

查找表: 在计算机中,是指被查找的数据对象是由同一类型的记录构成的集合,如顺序表,链表、二叉树和哈希表等

查找效率: 查找算法中的基本运算是通过记录的关键字与给定值进行比较,所以查找的效率同常取决于比较所花的时间,而时间取决于比较的次数。通常以关键字与给定值进行比较的记录个数的平均值来计算。

查找操作及分类:

  1. 操作

    • 查找某个“特定的”数据元素是否存在在查找表中
    • 某个“特定的”数据元素的各种属性
    • 在查找表中插入一个数据元素
    • 从查找表中删除某个数据元素
  2. 分类

    若对查找表只进行( 1 ) 或( 2 )两种操作,则称此类查找表为 静态查找表

    若在查找过程中同时插入查找表中存在的数据元素,或者从查找表中删除已存在的某个数据元素,则称此类查找表为 动态查找表

⛳(二)数组和索引

日常生活中,我们经常会在电话号码簿中查阅“某人”的电话号码,按姓查询或者按字母排序查询; 在字典中查阅“某个词”的读音和含义等等。在这里,“电话号码簿”和“字典”都可看作一张查找表, 而按“姓”或者“字母”查询则是按索引查询

索引: 索引把线性表分成若干块,每一块中的元素存储顺序是任意的,但是块与块间必须按关键字大小按顺序排列。即前一块中的最大关键字值小于后一块中的最小关键字值。

索引表: 分块以后,为了快速定义块,还需要建立一个索引表,索引表中的一项对应于线性表中的一块,索引项由键域和链域组成。键域存放相应关键字的键值,链域存放指向本块第一个节点和最后一个节点的指针,索引表按关键字由小到大的顺序排列!

例子:

1、数组是特殊的块索引(一个块一个元素):

在这里插入图片描述

2、哈希表是非常经典的块索引!

在这里插入图片描述

分块查找的算法分两步进行,首先确定所查找的节点属于哪一块,即在索引表中查找其所在的块,然后在块内查找待查询的数据。由于索引表是递增有序的,可采用二分查找,而块内元素是无序的,只能采用顺序查找。(块内元素较少,则不会对执行速度有太大的影响)

🚀二、二分查找

算法精炼: 二分查找法实质上是不断地将有序数据集进行对半分割,并检查每个分区的中间元素。再重复根据中间数确定目标范围并递归实行对半分割,直到中间数等于待查找的值或是目标数不在搜索范围之内

C语言代码实现:

//二分查找的循环方法
int BinaryResearch(int arr[],int len,int e)
{
	int left = 0;
	int right = len - 1;
	int mid = right / 2;
	while (left<right)
	{
		if (arr[mid] == e) return mid;
 
		if (e < arr[mid])
		{
			right = mid - 1;
			mid = (right + left) / 2;
		}	
		else
		{
			left = mid + 1;
			mid = (right + left) / 2;
		}	
	}
}
 
//二分查找的递归方法
int BinaryResearch(int arr[], int left,int right, int e)
{
	if (left < right)
	{
		int mid = (right + left) / 2;
		if (arr[mid] == e) return mid;
 
		if (e < arr[mid])
			return BinaryResearch(arr, left, mid - 1, e);
		else
			return BinaryResearch(arr, mid + 1, right, e);
	}
	
}

🚀三、穷举搜索

算法精炼: 列举出所有可能的情况,逐个判断有哪些是符合问题所要求的条件,从而得到问题的全部解答

它利用计算机运算速度快、精确度高的特点,对要解决问题的所有可能情况,一个不漏地进行检查,从中找出符合要求的答案。

分析步骤:

用穷举算法解决问题,通常可以从两个方面进行分析:

  1. 问题所涉及的情况:问题所涉及的情况有哪些,情况的种数必须可以确定。把它描述出来。应用穷举时对问题所涉及的有限种情形必须一一列举,既不能重复,也不能遗漏。重复列举直接引发增解,影响解的准确性;而列举的遗漏可能导致问题解的遗漏。
  2. 答案需要满足的条件:分析出来的这些情况,需要满足什么条件,才成为问题的答案。把这些条件描述出来。

C语言代码实现:

例子:有 20 枚硬币,可能包括 4 种类型: 1 元、 5 角、 1 角和 5 分。已知 20 枚硬币的总价值为 10 元,求各种硬币的数量。例如: 4 、 11 、 5 、 0 就是一种方案。而 8 、 2 、 10 、 0 是另一个可能的方案,显然方案并不是唯一的,请编写程序求出类似这样的不同的方案一共有多少种?

穷举思路:

直接对四种类型的硬币的个数进行穷举。其中,1 元最多 10 枚、5 角最多 20 枚、1 角最多20 枚、5 分最多 20 枚。

//避免出现小数,将1元化作100分
int CoinCount(int a1, int a2, int a3, int a4,int total,int size_max)
{
    //记录最多使用每种硬币多少个
	int max1 = total / a1 > size_max ? size_max : total / a1;
	int max2 = total / a2 > size_max ? size_max : total / a2;
	int max3 = total / a3 > size_max ? size_max : total / a3;
	int max4 = total / a4 > size_max ? size_max : total / a4;
 
	int i = 0;
	for (int size_a1 = 0; size_a1 < max1; size_a1++)
		for (int size_a2 = 0; size_a2 < max2; size_a2++)
			for (int size_a3 = 0; size_a3 < max3; size_a3++)
				for (int size_a4 = 0; size_a4 < max4; size_a4++)
                     if ((size_a1 * a1 + size_a2 * a2 + size_a3 * a3 + size_a4 * a4) == total
                         &&(size_a1+size_a2+size_a3+size_a4 == size_max))
                     {
                         i++;
                         printf(" %d:%d\t %d:%d\t %d:%d\t %d:%d\n",
                             a1, size_a1,
                             a2, size_a2,
                             a3, size_a3,
                             a4, size_a4);
                     }	
	return i;
}

🚀四、并行搜索

⛳(一)并发的基本概念

所谓并发是在同一实体上的多个事件同时发生。并发编程是指在在同一台计算机上“同时”处理多个任务。要理解并发编程,我们必须要理解如下一些基本概念:

  1. 计算机就像一座工厂,时刻在运行,为人类服务。它的核心是 CPU,它承担了所有的计算任务,就像工厂的一个现场指挥官。

  2. 进程就像工厂里的车间,承担“工厂”里的各项具体的“生产任务”,通常每个进程对应一个在运行中的执行程序,比如,QQ 和微信运行的时候,他们分别是不同的进程。

  3. 因为特殊原因,现场指挥官人才短缺,整个工厂只有一个指挥官,一次只能指导一个车间生产,而所有的车间都必须要有现场指挥官在场才能生产。也就是说,一个车间开工的时候,其他车间都必须停工。

    背后的含义:任一时刻,单个 CPU 一次只能运行一个进程,此时其他进程处于非运行状态

    我们能同时运行QQ、微信,是因为速度非常快,这种机制叫做”时间片“

  4. 一个车间(进程)可以包括多条生产线,线程就好比车间(进程)里的生产线。所有生产线(设备和人)都属于同一车间的资源,受车间统一调度和调配,并共享车间所有资源(如空间或洗手间)。

    背后的含义:一个进程可以拥有多个线程,每个线程可以可以独立并行执行,多个线程共享同一进程的资源,受进程管理。

理解了以上这些概念后,我们接下来再继续讲解并行搜索的概念:假设我们要从很大的一个无序的数据集中进行搜索,假设我们的机器可以一次性容纳这么多数据。从理论上讲,对于无序数据,如果不考虑排序,已经很难从算法层面优化了。而利用上面我们提到的并行处理思想,我们可以很轻松地将检索效率提升多倍。

具体实现思路如下:

将数据分成 N 个块,每个块由一个 线程来并行搜索。

⛳(二)线程演示代码

#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <time.h>

#define TEST_SIZE (1024*1024*200)
#define NUMBER 20

DWORD WINAPI ThreadProc(void *lpParam){
    for(int i=0; i<5; i++){
        printf("进程老爸,我来了!\n");
        Sleep(1000);
    }
    return 0;
}

int main(void){
    DWORD threadID1;//线程 1 的身份证
    HANDLE hThread1;//线程 1 的句柄
    
    DWORD threadID2;//线程 2 的身份证
    HANDLE hThread2;//线程 2 的句柄
    
    printf("创建线程... ... \n");
    
    //两个线程独立执行各自的函数后退出
    //创建线程 1
    hThread1 = CreateThread(NULL, 0, ThreadProc, NULL, 0, &threadID1);
    
    //创建线程 2
    //参数分别为:线程属性、线程堆栈的大小、线程启动后的执行函数、传递给线程函数的实参、线程标志、线程ID
    hThread2 = CreateThread(NULL, 0, ThreadProc, NULL, 0, &threadID2);
    
    //进程等待线程结束再继续执行,INFINITE表示时间无限大
    WaitForSingleObject(hThread1, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);
    
    printf("进程老爸欢迎线程归来!\n");
    system("pause");
    return 0;
}

拓展:完整示例代码:

#include <Windows.h>
#include <stdio.h>
#include <iostream>
#include <time.h>

#define TEST_SIZE (1024*1024*200)
#define NUMBER 20

typedef struct _search{
    int *data;//搜索的数据集
    size_t start; //搜索的开始位置
    size_t end; //搜索的终止位置
    size_t count; //搜索结果
}search;

//检索代码
DWORD WINAPI ThreadProc(void *lpParam){
    search *s = (search*)lpParam;
    time_t start, end;
    
    printf("新的线程开始执行...\n");
    
    time(&start);
    for(int j=0; j<10; j++){ //外层延时循环
        for(size_t i=s->start; i<=s->end; i++){
            if(s->data[i] == NUMBER){
            	s->count++;
        	}
    	}
    }
    time(&end);
    	
    printf("查找数据所花时间: %lld\n", end-start);
    return 0;
}

//并行代码(多线程)
int main02(void){
    int *data = NULL;
    int count = 0;//记录的数量
    int mid = 0;
    
    search s1, s2;
    
    data = new int[TEST_SIZE];
    
    for(int i=0; i<TEST_SIZE; i++){
    	data[i] = i;
    }
    
    mid = TEST_SIZE/2;
    s1.data = data;
    s1.start = 0;
    s1.end = mid;
    s1.count = 0;
    
    s2.data = data;
    s2.start = mid+1;
    s2.end = TEST_SIZE-1;
    s2.count = 0;
    
    DWORD threadID1;//线程 1 的身份证
    HANDLE hThread1;//线程 1 的句柄
    
    DWORD threadID2;//线程 2 的身份证
    HANDLE hThread2;//线程 2 的句柄
    
    printf("创建线程... ... \n");
    //创建线程 1
    hThread1 = CreateThread(NULL, 0, ThreadProc, &s1, 0, &threadID1);
    //创建线程 2
    hThread2 = CreateThread(NULL, 0, ThreadProc, &s2, 0, &threadID2);
    
    WaitForSingleObject(hThread1, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);
    
    printf("进程老爸欢迎线程归来!count: %d\n", s1.count+s2.count);
    system("pause");
    return 0;
}

//串行代码
int main(void){
    int *data = NULL;
    int count = 0;//记录的数量
    
    data = new int[TEST_SIZE];
    
    for(int i=0; i<TEST_SIZE; i++){
    	data[i] = i;
    }
    
    time_t start=0, end=0;//记录开始和结束的时间戳
    
    time(&start);
    for(int j=0; j<10; j++){
        for(int i=0; i<TEST_SIZE; i++){
            if(data[i] == NUMBER){
            	count++;
            }
        }
    }
    
    time(&end);
    printf("查找数据所花时间: %lld, count: %d\n", end-start, count);
    
    system("pause");
    return 0;
}
;