查找的相关概念
查找是什么呢?
- 我准备打个电话给朋友,懒得重新输入电话号码,需要查找通讯录或者通话记录;
- 我想买件衣服或者买双鞋,需要在网购app上搜索关键字,来查找心仪的商品;
- 我LOL想练一个没玩过的英雄,不知道该点什么天赋,出什么装备,需要去浏览器去查找玩法攻略;
- …
由此可见,查找或搜索,在我们的生活中随处可见,甚至说是必不可少的。所以我们来讲一下查找的相关概念:
在数据结构中,我们把所有被查的数据所在的集合,统称为查找表
查找表(Search Table)是 同一类型 的数据元素(或记录)构成的集合。
下图就是一个查找表
我们一般对查找表进行以下几种操作:
- 查询某个 “特定的” 数据元素是否在查找表中
- 检索某个 “特定的” 数据元素的各种属性
- 在查找表中插入一个数据元素
- 从查找表中删去某个数据元素
对于只实现前两种操作的查找表,称为 静态查找表,
以上四种均可以实现的查找表,称为 动态查找表。
本文会着重讲解静态查找表的两种查找算法:顺序查找和折半查找
关键字(Key)是数据元素(或记录)中某个数据项的值,用它可以标识(识别)一个数据元素(或记录)。
若此关键字可以惟一地标识一个记录,则称此关键字为主关键字(Primary Key)
(对不同的记录,其主关键字均不同)。
反之,称用以识别若干记录的关键字为次关键字(SecondaryKey)。当数据元素只有一个数据项时,其关键字即为该数据元素的值。
排序的相关概念
说完查找,就不得不说一下排序了。
排序,顾名思义,就是排列顺序。排序顺序的方式有很多种啊
比如说:我想在淘宝上,买一支笔
淘宝上对商品的排序有以下四种方式(默认是综合排序)
-
假如我只想要一支可以写字的笔就行,越便宜越好,那么我把排序方式调整为价格升序(从小到大)
-
假如我只想买贵的,我觉得买贵的能让我写文章或者工作时更有感觉!那就调整为价格降序(从大到小)
-
假如我又不想买太便宜的,怕质量不好,又不像花太大的开销。那么就点综合吧,性价比最高!
顺便提一下,我最喜欢的笔是施华洛世奇的水晶笔!!!太太太可了!!!
言归正传,排序在生活中的应该还有很多,成绩单上的分数排名、字典上的拼音顺序、站队列的高矮顺序…
排序(Sorting)是计算机程序设计中的一种重要操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列。
假设含有n个记录的序列为{r1,r2,……,rn}
其相应的关键字分别为{k1,k2,……,kn}
需确定1, 2, … n的一
种排列p1,p2,……pn,使其相应的关键字满足kp1≤kp2≤……≤kpn (非递减或非递增)关系,即使得序列成为一个按关键字有序的序列{rp1,rp2,……,rpn},这样的操作就称为排序。
注意我们在排序问题中,通常将数据元素称为记录。显然我们输入的是一个记录集合,输出的也是一个记录集合,所以说,可以将排序看成是线性表的一种操作。
排序的依据是关键字之间的大小关系,那么,对同一个记录集合,针对不同的关键字进行排序,可以得到不同序列。
排序的稳定性
也正是由于排序不仅是针对主关键字,那么对于次关键字,因为待排序的记录序列中可能存在两个或两个以上的关键字相等的记录,排序结果可能会存在不唯一的情况,我们给出了稳定与不稳定排序的定义。
假定在待排序的记录序列中, 存在多个具有相同关键字的记录, 若经过排序, 这些记录的相对次序保持不变。
通俗来讲就是,在原序列中, a=b, 且a在b之前, 而排序后, a仍在b之前, 则称为这种排序算法是稳定的, 否则称为不稳定的。
内排序和外排序
根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序。
内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。
外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
本文着重介绍内排序的多种方法。对于内排序来说,排序算法的性能主要是受3个方面影响:
- 时间性能
- 辅助空间
- 算法的复杂性
本文着重讲解四种排序的算法:冒泡排序、简单选择排序、直接插入排序和快速排序。
查找
首先定义和声明一下之后会用到的宏
首先是定义顺序表的存储结构以及创建和输出顺序表,还有一个很重要,后面会经常调用的函数——swap交换函数,在排序算法里面会经常使用的。
#include<iostream>
using namespace std;
#define MAXSIZE 20 //最大表长
typedef int KeyType;
typedef int ElemType;
typedef struct
{ //顺序表的存储结构
ElemType r[MAXSIZE + 1]; //用于存储要进行操作的数组,r[0]作为哨兵或者临时变量
int length; //数组长度
}SElemType;
void swap(SElemType& ST, int i, int j)
{ //交换数组中下标为i和j的两个元素
int t;
t = ST.r[i];
ST.r[i] = ST.r[j];
ST.r[j] = t;
}
int Create(SElemType& ST)
{ //创建一个顺序表
int n;
cout << "请输入元素的个数:";
cin >> n;
ST.length = n;
for (int i = 1; i <= n; i++)
cin >> ST.r[i];
return 0;
}
void Print(SElemType ST)
{ //输出顺序表
int i;
for (i = 1; i <= ST.length; i++)
cout << ST.r[i] << " ";
}
顺序查找
顺序查找又叫线性查找,是最最最基本的查找技术,其过程如下:
从表的最后一个记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到第一个记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
我们在算法中,把顺序表的数组ST中,第一个元素为ST[1]而不是ST[0],因为ST[0]在这里作为“哨兵”,从最后一个元素开始,往前一个一个的查找,若都找不到,就遍历到ST[0]了,也就是查找失败了(查找失败时,返回数值0)。
int search(SElemType ST, KeyType key)
{ //直接查找法
int i;
ST.r[0] = key; //设置数组0为哨兵,即查找失败时,返回0
for (i = ST.length; ST.r[i] != key; i--);
return i;
}
折半查找
折半查找,它的过程很类似于我们高中学过的二分法,所以又称为二分查找。
折半查找法的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。
折半查找的基本思想是:
在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。
一定要注意!!!折半查找的前提是,一定要是有序表,即排好序了的表
故要在排序之后才能执行的查找算法
int SearchBin(SElemType ST, KeyType key)
{ //折半查找法(前提是表为有序表)
int mid, low, high;
low = 1;
high = ST.length;
while (low <= high)
{
mid = (low + high) / 2;
if (ST.r[mid] == key) return mid;
else if (key < ST.r[mid]) high = mid - 1; //在前半区间继续查找
else low = mid + 1; //在后半区间继续查找
}
return 0; //若查找失败,则返回值为0
}
排序
直接插入排序
直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
这里的ST[0]的作用是作为监视哨,用来存放每一趟插入的元素
void InsertSort(SElemType& ST)
{ //直接插入排序法(从小到大)
int i, j;
for (i = 2; i <= ST.length; i++)
{
if (ST.r[i] < ST.r[i - 1])
{
ST.r[0] = ST.r[i]; //复制为监视哨,用来存放要插入的元素
ST.r[i] = ST.r[i - 1];
for (j = i - 2; ST.r[0] < ST.r[j]; j--)
ST.r[j + 1] = ST.r[j]; //如果该位置比监视哨大,往后移一位
ST.r[j + 1] = ST.r[0]; //插入到正确的位置
}
}
}
简单选择排序
选择排序的基本思想是:每一趟在n-i+1计1(i=1,2,…,n- 1)个记录中选取关键字最小的记录作为有序序列中第i个记录。
一趟简单选择排序的操作为:通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之。
我在网上看到一张gif,可以很完美地诠释简单选择排序的过程
void SelectSort(SElemType& ST)
{ //简单选择排序
int i,j;
int min;
for (i = 1; i <= ST.length; i++)
{
min = i; //把每一趟的min初值赋值为i
for (j = i + 1; j <= ST.length; j++) //从i的后一个元素开始,依次进行比较
{
if (ST.r[j] < ST.r[min]) min = j; //如果有元素小于ST.r[min],则该元素的下标变成min
}
if (i != min) //如果min不等于i,则说明有元素比ST.r[i]要小,且找到了这一趟中最小的一个,将它与i替换位置
swap(ST, i, min);
}
}
冒泡排序
冒泡排序可能是我们早就在刚刚进入编程世界,接触c语言时,就已经学习了的排序方法。思路很简单,很好理解,顾名思义,冒泡,就是轻的浮起来,重的就沉下去了。
它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
void BubbleSort(SElemType& ST)
{ //冒泡排序法(从小到大的排序)
int i, j;
for (i = 1; i < ST.length; i++)
for (j = ST.length - 1; j >= i; j--) //j从后往前循环
{
//j起始为倒数第二个数,跟它的后一个数进行比较,若后一个数小于前一个数,交换(可以想象成冒泡泡,小的也就是轻的,浮上去了)
if (ST.r[j] > ST.r[j + 1])
swap(ST, j, j + 1);
}
}
但是大家在实现这个函数的过程中,会发现一个问题,如果出现如下图的情况,即只有前面几位是乱序的,后面都是排好序了,实际上只用交换前几个即可,但是还是之后的多次循环仍要执行,这就很浪费时间和执行次数
于是我们可以增加一个布尔变量flag作为标记变量,用来标记在每一趟中是否发生过交换。
首先在每一趟交换前令flag=false,只要在这一趟中发生了交换,就让flag=true,每一趟循环的开始依据是判断flag是否为true
只要有一趟循环中没有发生交换,就说明交换已完成,可以停止了,避免不必要的过程
这是改进后的算法函数:
void BubbleSort_plus(SElemType& ST)
{ //冒泡排序法的改进 (当有一趟是没有进行交换的时候,排序已经完成,就可以停止了,避免不必要的过程
int i, j;
for (i = 1; i < ST.length; i++)
{
bool flag = false; //添加一个布尔变量作为标记变量,默认为false
for (j = ST.length - 1; j >= i; j--)
{
if (ST.r[j] > ST.r[j + 1])
{
swap(ST, j, j + 1);
flag = true; //只要有交换 flag就为true
}
}
if (!flag) break; //如果flag不为true,即在一趟中没有交换,则说明排序已完成,立即结束
}
}
快速排序
快速排序,从命名就可以看出来,这个算法是十分了不起的,只有它敢自称“快速”,但凡有一个函数比它要更快,那么快速排序就会名不符实。
当然,至少在今天,在多次优化后的快速排序法,在整体性能上,依然是排序算法中的王者!
下面来讲一下快速排序 的基本思想:
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分 记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
这里该如何将待排序的记录分为独立的两个部分呢?我们会将每一趟排序的第一个元素,设置为枢轴,让枢轴左边的元素都小于它,右边的元素都大于它,这样就分为了左半区间和右半区间。
然后在左半区间和右半区间继续执行同样的操作。
一趟快速排序的具体做法是:附设两个指针low和high,它们的初值分别为low 和high,设枢轴记录的关键字为pivotloc,则首先从high所指位置起向前搜索找到第一个关键字小于pivotloc的记录和枢轴记录互相交换,然后从low所指位置起向后搜索,找到第一个关键字大于pivotloc的记录和枢轴记录互相交换,重复这两步直至low= high,算法停止
这里要运用到递归的思想
void QuickSort(SElemType& ST)
{
//快速排序法
QSort(ST, 1, ST.length);
}
这里执行QSort函数
void QSort(SElemType& ST, int low, int high)
{
//快速排序法的递归实际实现
int pivotloc; //定义一个枢轴,其作用是将顺序表分成两个部分,左半区间都小于枢轴,右半区间都大于枢轴
if (low < high)
{
pivotloc = Partition(ST, low, high);
QSort(ST, low, pivotloc - 1);
QSort(ST, pivotloc + 1, high);
}
}
下面是算法的核心,将表分为左半区间和右半区间,并确定枢轴
int Partition(SElemType& ST, int low, int high)
{
//枢轴功能的具体实现
//将枢轴放在表中的位置,将顺序表分成两个部分,左半区间都小于枢轴,右半区间都大于枢轴
int pivotloc;
pivotloc = ST.r[low];
while (low < high)
{
while (low < high && ST.r[high] >= pivotloc) high--;
swap(ST, low, high); //比枢轴小的交换到低端
while (low < high && ST.r[low] <= pivotloc) low++;
swap(ST, low, high); //比枢轴大的交换到高端
}
return low;
}
但是,就这个交换而言,每一次的交换都是在调用交换函数,这会使算法性能大大降低。我们为什么不能用移动来代替交换呢?然后把枢轴用ST.r[0]备份,这样在每一趟的最后,也可以达到预期的效果。
所以我们对核心函数进行改进:
int Partition_plus(SElemType& ST, int low, int high)
{
//枢轴功能的具体实现(改进算法 ,将交换改成了移动,减少了不必要的交换,仅需要移动即可)
//将枢轴放在表中的位置,将顺序表分成两个部分,左半区间都小于枢轴,右半区间都大于枢轴
ST.r[0] = ST.r[low]; //r[0]用于储存枢轴
int pivotloc = ST.r[low];
while (low < high)
{
while (low < high && ST.r[high] >= pivotloc) high--;
ST.r[low] = ST.r[high]; //比枢轴小的“移动”到低端
while (low < high && ST.r[low] <= pivotloc) low++;
ST.r[high] = ST.r[low]; //比枢轴大的“移动”到高端
}
ST.r[low] = ST.r[0]; //把因为“移动”而空缺的枢轴位置填上,该位置就是枢轴应该在的位置
return low;
}
主函数
int main()
{
SElemType a;
int i, j, k;
cout << "1.创建顺序表 2.输出顺序表 3.排序 4.查找 0.退出" << endl;
while (1)
{
cout << "请选择想要执行的操作:";
cin >> i;
switch (i)
{
case 1: Create(a); break;
case 2: Print(a); cout << endl; break;
case 3: cout << " 1.直接插入排序法 2.冒泡排序法 3.快速排序法 4.简单选择排序法" << endl;
cout << "请选择一种排序法进行排序:";
cin >> j;
switch (j)
{
case 1:InsertSort(a); cout <<"已经完成排序"<< endl; break;
case 2:BubbleSort_plus(a); cout << "已经完成排序" << endl; break;
case 3:QuickSort(a); cout << "已经完成排序" << endl; break;
case 4:SelectSort(a); cout << "已经完成排序" << endl; break;
default:cout << "请输入正确的内容!重新开始选择第一步的操作!" << endl; break;
}break;
case 4:cout << "请输入要查找的元素:"; cin >> k; cout << "第" << SearchBin(a, k) << "个元素"; cout << endl; break;
case 0:exit(0); break;
default:cout << "请输入正确的内容" << endl; break;
}
}
return 0;
}
数据结构的篇章更新可能就告一段落了,未来可能会有一些好的实用的算法的讲解,希望可以帮助到大家。
眼里有光的人,是不会被阴暗吞噬的
- 有时候走过一些弯路,也好过原地踏步。