一、查找的基本概念
查找(Searching):就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素( 或记录)。
查找表(Search Table):是由同一类型的数据元素(或记录)构成的集合。
关键字(Key):数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。例如,在由一个学生元素构成的数据集合中,学生元素中“学号”这一数据项的值唯一地标识一名学生。
静态查找表(Static Search Table):只作查找操作的查找表。
- 主要操作
- 查询某个“特定的”数据元素是否在查找表中。
- 检索某个“特定的”数据元素和各种属性。
平均查找长度:在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度,则是所有查找过程中进行关键字的比较次数的平均值,其数学定义为
A
S
L
=
∑
i
=
1
n
P
i
C
i
ASL=\sum^n_{i=1}P_iC_i
ASL=i=1∑nPiCi
这个式子中,n是查找表的长度;
P
i
P_i
Pi是查找第
i
i
i个数据元素的概率,一般认为每个数据元素的查找概率相等,即
P
i
=
1
n
P_i=\frac{1}n
Pi=n1;
C
i
C_i
Ci是找到第
i
i
i个数据元素所需进行的比较次数。平均查找长度是衡量查找算法效率的最主要的指标。
二、顺序表查找
1.定义
顺序查找(Sequential Search) 又叫线性查找,是最基本的查找技术,作为一种最直观的查找方法,其基本思想是从线性表的一端开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置;若已经查找到表的另一端,但还没有查找到符合给定条件的元素,则返回查找失败的信息。
2.算法
代码如下:
平均查找长度:在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度,则是所有查找过程中进行关键字的比较次数的平均值,其数学定义为
A
S
L
=
∑
i
=
1
n
P
i
C
i
ASL=\sum^n_{i=1}P_iC_i
ASL=i=1∑nPiCi
这个式子中,n是查找表的长度;
P
i
P_i
Pi是查找第
i
i
i个数据元素的概率,一般认为每个数据元素的查找概率相等,即
P
i
=
1
n
P_i=\frac{1}n
Pi=n1;
C
i
C_i
Ci是找到第
i
i
i个数据元素所需进行的比较次数。平均查找长度是衡量查找算法效率的最主要的指标。
二、顺序表查找
1.定义
顺序查找(Sequential Search) 又叫线性查找,是最基本的查找技术,作为一种最直观的查找方法,其基本思想是从线性表的一端开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置;若已经查找到表的另一端,但还没有查找到符合给定条件的元素,则返回查找失败的信息。
2.算法
代码如下:
/*有哨兵顺序查找*/
int Sequential_Search(int *a, int n, int key){
int i;
a[0] = key; //设置a[0]为关键字,称之为“哨兵”
i = n; //循环从数组尾部开始
while(a[i] != key){
i--;
}
return i; //返回0则说明查找失败
}
三、顺序查找的应用
1.查找最大值和最小值
查找顺序表中的最大值和最小值,比较次数不超过 3 2 n \frac32 n 23n
算法1:朴素查找法——比较次数:2(n-1)
//返回一个数组,数组第一个元素为最大值,第二个元素为最小值
int res[2];
int* findmaxandmin(int arr[],int n){
int max=arr[0];
int min=arr[0];
for(int i=1;i<n;i++){
if(max<arr[i]) max=arr[i];
if(min>arr[i]) min=arr[i];
}
res[0]=max;
res[1]=min;
return res;
}
算法2:快速查找法——比较次数 3 2 n \frac32 n 23n
先比较两者,再用较小值和较大值分别与最小值和最大值比较
//返回一个数组,数组第一个元素为最大值,第二个元素为最小值
int res2[2];
int* findmaxandmin2(int arr[],int n){
int max=arr[0];
int min=arr[0];
int k=n%2;//n是奇数,k从1开始,否则从0开始
while (k<n)
{
if(arr[k]<arr[k+1]){
if(min>arr[k]) min=arr[k];
if(max<arr[k+1]) max=arr[k+1];
}
else{
if(min>arr[k+1]) min=arr[k+1];
if(max<arr[k]) max=arr[k];
}
k=k+2; //每次同时比较两个元素
}
res2[0]=max;
res2[1]=min;
return res2;
}
2.查找区间内所有质数
查找正整数区间[1,n](n>1)内所有质数。
(1).试除法 ——时间复杂度 O ( n n ) O(n\sqrt n) O(nn)
代码如下:
#include <iostream>
#include <cmath>
using namespace std;
// 判断一个数是否是质数的函数
bool isPrime(int num) {
// 1和负数都不是质数
if (num <= 1) return false;
// 2是唯一的偶数质数
if (num == 2) return true;
// 其他偶数都不是质数
if (num % 2 == 0) return false;
// 从3开始,每次加2,只检查奇数因子
for (int i = 3; i <= sqrt(num); i += 2) {
// 如果有任何因子可以整除,就不是质数
if (num % i == 0) return false;
}
// 没有找到任何因子,就是质数
return true;
}
int main() {
// 输入n的值
int n;
cout << "请输入n的值:";
cin >> n;
// 遍历区间[1,n],输出所有质数
cout << "区间[1," << n << "]内的所有质数如下:" << endl;
for (int i = 1; i <= n; i++) {
if (isPrime(i)) {
cout << i << " ";
}
}
cout << endl;
return 0;
}
(2)埃氏筛选法——时间复杂度:O(nloglog(n))
代码如下:
#include <iostream>
#include <vector>
using namespace std;
// 用埃式筛选法查找正整数区间[1,n]内所有质数的函数
vector<int> eratosthenes(int n) {
vector<int> primes; // 存储找到的素数
vector<bool> is_prime(n + 1, true); // 标记每个数是否是素数,初始都为true
is_prime[0] = is_prime[1] = false; // 0和1不是素数,标记为false
for (int i = 2; i <= n; i++) {
if (is_prime[i]) { // 如果i是素数,就把它加入到primes中
primes.push_back(i);
// 用i的倍数从i*i开始筛掉所有能被i整除的数,因为小于i*i的合数已经被更小的素数筛掉了
for (int j = i * i; j <= n; j += i) {
is_prime[j] = false;
}
}
}
return primes; // 返回素数的向量
}
int main() {
// 输入n的值
int n;
cout << "请输入n的值:";
cin >> n;
// 调用eratosthenes函数,得到区间[1,n]内的所有素数
vector<int> primes = eratosthenes(n);
// 输出素数
cout << "区间[1," << n << "]内的所有素数如下:" << endl;
for (int p : primes) {
cout << p << " ";
}
cout << endl;
return 0;
}
(3)合数限定法——时间复杂度:O(n)
代码:略
(4)欧拉筛选法——时间复杂度:O(n)
代码如下:
#include <iostream>
#include <vector>
using namespace std;
// 用欧拉筛选法查找正整数区间[1,n]内所有质数的函数
vector<int> euler(int n) {
vector<int> primes; // 存储找到的素数
vector<bool> is_prime(n + 1, true); // 标记每个数是否是素数,初始都为true
is_prime[0] = is_prime[1] = false; // 0和1不是素数,标记为false
for (int i = 2; i <= n; i++) { // i从2循环到n(外层循环)
if (is_prime[i]) primes.push_back(i); // 如果i没有被前面的数筛掉,则i是素数,加入到primes中
for (int j = 0; j < primes.size() && i * primes[j] <= n; j++) { // 筛掉i的素数倍,即i的primes[j]倍
// j循环枚举现在已经筛出的素数(内层循环)
is_prime[i * primes[j]] = false; // 倍数标记为合数,也就是i用primes[j]把i * primes[j]筛掉了
if (i % primes[j] == 0) break; // 最神奇的一句话,如果i整除primes[j],退出循环
// 这样可以保证线性的时间复杂度
}
}
return primes; // 返回素数的向量
}
int main() {
// 输入n的值
int n;
cout << "请输入n的值:";
cin >> n;
// 调用euler函数,得到区间[1,n]内的所有素数
vector<int> primes = euler(n);
// 输出素数
cout << "区间[1," << n << "]内的所有素数如下:" << endl;
for (int p : primes) {
cout << p << " ";
}
cout << endl;
return 0;
}
四、有序表查找
1.折半查找
折半查找(Binary Search)技术,又称为二分查找。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。
代码如下:
//实现方法1
int BinarySearch1(int arr[],int left,int right,int key){
int low=left;
int high=right;
int mid;
while(low<=high){
mid = (low + high)/2; //取中间位置
if(arr[mid] == key){
return mid; //查找成功返回所在位置
}else if(arr[mid] > key){
high = mid - 1; //从前半部分继续查找
}else{
low = mid + 1; //从后半部分继续查找
}
}
return -1; //查找失败,返回-1
}
//实现方法2
int BinarySearch2(int arr[],int left,int right,int key){
int low=left-1;
int high=right+1;
int mid;
while(high>low+1){
mid = (low + high)/2; //取中间位置
if(arr[mid] == key){
return mid; //查找成功返回所在位置
}else if(arr[mid] > key){
high = mid ; //从前半部分继续查找
}else{
low = mid ; //从后半部分继续查找
}
}
return -1; //查找失败,返回-1
}
2.二分查找的应用
1.区间查询
代码如下:
int SearchinInterval(int arr[],int left,int right,int key){
int low=left-1;
int high=right+1;
int mid;
while(high>low+1){
mid = (low + high)/2; //取中间位置
if(arr[mid] >= key){
high = mid ;
}else{
low = mid ; //从后半部分继续查找
}
}
return high;//或者 low; // high :>=key的最小值位置 low:<key的最大值位置
}
2.快速求幂
给定正整数a和n,求 a n a^n an的值
法一:直接迭代: a n = a n − 1 ∗ a a^n=a^n-1*a an=an−1∗a ——时间复杂度O(n)
法二:二分递归: a n = a n 2 ∗ a n 2 ∗ a n % 2 a^n=a^{\frac n2}*a^{\frac n2}*a^{n\%2} an=a2n∗a2n∗an%2——时间复杂度O(log(n))
代码如下:
int Power(int x,int n){
if(n==1){
return x;
}
int pow=Power(x,n/2);
pow=pow*pow;
if(n%2==1){
pow=pow*x;
}
return pow;
}
3.快速查找
查找未排序序列 < a 1 , a 2 . … , a n > <a_1,a_2.\dots,a_n> <a1,a2.…,an>中的第k小元素
法一: 循环k次选择排序或冒泡排序 ——时间复杂度O(kn)
法二:快速或递归排序——时间复杂度:O(nlogn)[排序]+O(1)[找第k小元素]
法三: 快速建最小堆+k次出堆——时间复杂度:O(n)+O(klogn)
法四:利用快速排序的partition过程,进行二分——时间复杂度:O(n)
法四,代码如下:(注意k输入时要-1)
void swap(int* a,int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int Partition(int* a,int begin,int end)
{
int L = begin,R = end,key = begin;//定义L,R,key下标
while(L < R)
{
while(a[R] >= a[key] && L < R)//右边先走,右边大于等于key就一直走下去,否则停下来,加上判断L < R,防止越界
{
R--;
}
while(a[L] <= a[key] && L < R)//同理
{
L++;
}
swap(&a[L],&a[R]);//都停下来后交换
}
swap(&a[key],&a[R]);//最后key值和相遇点交换
return R;
}
int QuickSearch(int arr[],int left,int right, int k){
if(left<right){
int m=Partition(arr,left,right);
if(m==k) return arr[m];
if(m<k) {
return QuickSearch(arr,m+1,right,k);
}
else{
return QuickSearch(arr,left,m-1,k);
}
}
return arr[left];
}