chapter3-排序和查找

1.排序

1.1基础排序

排序就是把一组无序的数据变成有序的过程。对于机试而言,直接使用C++封装好的sort函数就可以了。sort函数内部采用快速排序实现,因此非常高效。使用sort函数,需要引用#include <algorithm>头文件,sort(first_address, last_address, compare)有三个参数,first和last待排序序列的起始地址和结束地址;compare为比较方式。如果不规定第三个参数,则默认是从小到大升序方式。
注意,1、sort函数的地址是左闭右开区间的。2、如果要对给定的数组进行降序排列或以自定义的方式排列,那么第三个参数会起作用。可以通过编写比较函数的方式来实现想要的自定义排序方式。

排序,既包括对内部已经定义的基本数据类型的排序,如整型、字符型等,也包括对自定义数据类型的排序,如结构体或者类。所以要注意,整数是有大小关系的,而结构体变量本身是没有大小关系的,sort函数的默认排序方式无法满足要求,必须依据题面描述写一个自定义的函数才能完成上述要求。
考生在遇到新的排序规则时,只需记住如下这条黄金法则:当比较函数的返回值为true时,表示的是比较函数的第一个参数将会排在第二个参数前面。
所有的比较,最终还是要转换成为对于大小的比较,奇偶性就是考察模2之后值的大小比较,一切比较都是基于两个数之间如何定义它们的大小关系,找出内部的大小关系,设计出合格的Compare比较函数。

1.1整数奇偶排序

奇偶排序_代码实现
//排序-习题3.2 整数奇偶排序 2024-02-09
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN=10+2;
int arr[MAXN];
//自定义比较函数,确定谁在前谁在后
bool compare(int a, int b)
{
if(a%2 == 1 && b%2 == 1)
{
return b < a;
}else if (a%2 == 0 && b%2 == 0)
{
return a < b;
}else if (a%2 == 1 && b%2 == 0)
{
return true;
}else{//情况4:a为偶数,b为奇数
return false;
}
}
int main()
{
while(scanf("%d", &arr[0]) != EOF)
{
for(int i=1; i<10; i++)
{
scanf("%d", &arr[i]);
}
sort(arr, arr + 10, compare);//注意:sort函数的地址参数是左闭右开的
for(int i=0; i<10; i++)
{
printf("%d", arr[i]);
if(i != 9)
printf(" ");
}
printf("\n");
}
return 0;
}

1.2扩展排序——如何根据一些排序算法的特性去求解特殊的问题。

1.2.1问题1:线性排序

即在线性O(n)时间内实现对一个序列的排序,这就需要用到计数排序的特性。
我们知道,基于比较实现的排序时间复杂度下届是O(nlogn),那么要实现线性时间排序,就不能基于比较,而是用计数排序。注意使用计数排序的前提是,给定序列的所有数据是有一个范围的,开一个和数据范围一样大的辅助数组进行计数。

计数排序
//计数排序-杭州电子科技大学1425
//2024-02-14
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int MAXN = 1e6+10;
const int RANGE = 5e5;
int arr[MAXN]={0};//有多少个数据的范围
int tmp[MAXN]={0};//数据本身的范围。辅助数组,用于计数
int main()
{
int n,m;
while(scanf("%d%d", &n, &m) != EOF)
{
memset(tmp, 0, sizeof(tmp)); //因为有多组测试数据,每次要清空辅助数组
for(int i=0; i<n; i++)
{
scanf("%d", &arr[i]);
tmp[arr[i]+RANGE]++; //偏移5e5计数
}
int index = 0;
for(int i=0; i < MAXN; i++)
{
while(tmp[i]--)//把计数数组对应下标i存回原数组
{
arr[index++] = i - RANGE;
}
}
for(int i=n-1; i >= n-m; i--) //从后往前输出前m大的数
{
if(i == n - m)//输出格式控制,不错
{
printf("%d\n", arr[i]);
}
else{
printf("%d ", arr[i]);
}
}
}
return 0;
}
注意,1、因为有多组测试数据,所以辅助数组每组新的数据输入前要清空置0,用memset函数,memset()可以应用在将一段内存初始化为某个值。例如:

memset( the_array, '\0', sizeof(the_array) );这是将一个数组的所有分量设置成零的很便捷的方法。使用memset要引用头文件#include <string.h>(C语言)或者#include <cstring>(C++中使用)。
2、辅助数组计数完之后,为了后续的使用,也为了输出的便捷性,可以把辅助数组下标(即数据)再存回原数组。
3、杭电OJ很奇怪,我用G++编译运行会报超时错误,同样的代码在C++编译运行就通过了,不过注意一下初始化一个1e6次的数组是占用一定时间的,所以对于大的数组可以不初始化。

1.2.2问题2:求逆序数对

即如何快速求出一个序列的逆序数,这个问题需要用到归并排序的特点。
对一个无序序列归并成有序序列,如图所示,先拆分直至只剩一个元素,这时仅含单个元素的子序列就是有序的,然后合并,不断地把两个有序子序列合成一个更大的有序子序列,最终完成一整个序列的排序。在这个过程中,每一趟拆分或者合并都是线性时间O(n)完成的,一共2logn趟。算法时间复杂度最好、最坏都是O(nlogn)。
mergesort2.jpg
合并的过程如图。当后一个有序子序列的元素比前一个子序列的某个元素小时,就产生了逆序数,值为middle + 1 - i
combine.jpg

归并排序+计算逆序数对
//归并排序的同时附带求逆序数对 2024-02-14
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 1000 + 10;
int arr[MAXN];
int temp[MAXN];
int number; //用于计数,记录逆序数个数
//把两个有序子序列,合并成为一个更大的有序序列
void Combine(int left, int middle, int right)
{
int i = left,j = middle + 1;
int k = left; //temp数组的索引
while(i <= middle && j <= right)
{
if(arr[i] <= arr[j]){
temp[k++] = arr[i++];
}
else{
number += middle + 1 - i;
temp[k++] = arr[j++];
}
}
while(i <= middle)
{
temp[k++] = arr[i++];
}
while(j <= right)
{
temp[k++] = arr[j++];
}
//最后把合并完的有序序列存回原始数组arr
for(k= left; k <= right; k++)
{
arr[k] = temp[k];
}
}
void MergeSort(int left, int right)
{
//注意:不能写while(left < right),这样递归没有出口,永远不会跳出函数栈,陷入死循环!!!
if(left < right)//拆分直至单个元素必为有序子序列
{
// middle = (left + right) / 2; //这种写法存在溢出问题
int middle = left + (right - left) / 2;
MergeSort(left, middle);
MergeSort(middle+1, right);
Combine(left, middle, right);
}
}
int main()
{
int n;
scanf("%d", &n);
number = 0;
for(int i=0; i<n; i++)
{
scanf("%d", &arr[i]);
}
MergeSort(0,n-1);
for(int i=0; i<n; i++)//输出排序后的结果
{
if(i == n-1){
printf("%d\n", arr[i]);
}
else{
printf("%d ", arr[i]);
}
}
//实现在归并排序中附带计算逆序数的大小
printf("该序列的逆序数为:%d\n", number);
return 0;
}

1.2.3问题3:第K大数

即如何快速求出一个序列中第K大的数,需要用到快速排序的特性。
使得能够在线性O(n)的时间内获得结果。而朴素的想法是先通过排序,将无序序列变为有序序列,再定位数组arr[n-k]即为第K大的数,时间复杂度为O(nlogn)。
快速排序和归并排序一样同样通过递归实现。通过取枢轴元素,将整个无序序列一分为二,枢轴左边的元素都比它小,枢轴右边的元素都比它大;在左边的子序列中重复操作,即再取一个子序列的枢轴元素,一分为二,枢轴左边的元素都比它小,枢轴右边的元素都比它大;右边子序列同样;子序列的子序列只要还有1个以上的元素,就继续上述操作,直至序列中只剩一个元素,此时必定有序,也就是到了递归的出口条件left==right
qsort.jpg
依据上图,写出快排的实现。

快速排序
//快速排序实现 2024-02-14
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int MAXN = 1000 + 10;
int arr[MAXN];
//确定序列的枢轴元素,并遍历使枢轴左边的元素都比它小,右边都比它大
int Partition(int left, int right)
{
int pos = left + rand() % (right - left + 1);//得到[left, right]区间内的一个随机数
swap(arr[left], arr[pos]); //保证快排性能
int pivot = arr[left];//枢轴
while(left < right)
{
while(left < right && arr[left] < arr[right])
{
right--;
}
swap(arr[left], arr[right]);
while(left < right && arr[left] < arr[right])
{
left++;
}
swap(arr[left], arr[right]);
}
return left;
}
void QuickSort(int left, int right)
{
if(left < right)
{
int position = Partition(left, right);
QuickSort(left, position -1);
QuickSort(position + 1, right);
}
}
int main()
{
int n;
scanf("%d", &n);
for(int i=0; i<n; i++)
{
scanf("%d", &arr[i]);
}
QuickSort(0, n-1);
for(int i=0; i<n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}

进一步,我们如何利用快排的特性,在O(n)线性时间内找到无序序列中第K大的数呢?设无序序列总共有n个元素,第K大的数在有序序列中的数组下标就应该是n-k,而在每一轮的快排中,我们随机指定的枢轴元素都能找到它的最终位置,如图所示。故假如该枢轴元素刚好下标就是n-k,那么我们就找到了第K大的数,算法结束;假如此轮枢轴元素下标比n-k小,那么我们也能判断出第K大的数必定在枢轴的右边,从而排除掉近一半的元素(根据快排性能);假如枢轴下标比n-k大,则说明要找的第K大的数在枢轴的左边,继续只对左边的子序列进行快排即可,排除右边子序列。
qsort2.jpg

O(n)时间内找到第K大的数
//利用快排逻辑实现O(n)找到无序序列第K大的数 2024-02-14
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int MAXN = 1000 + 10;
int arr[MAXN];
//确定序列的枢轴元素,并遍历使枢轴左边的元素都比它小,右边都比它大
int Partition(int left, int right)
{
int pos = left + rand() % (right - left + 1);//得到[left, right]区间内的一个随机数
swap(arr[left], arr[pos]); //保证快排性能
int pivot = arr[left];//枢轴
while(left < right)
{
while(left < right && arr[left] < arr[right])
{
right--;
}
swap(arr[left], arr[right]);
while(left < right && arr[left] < arr[right])
{
left++;
}
swap(arr[left], arr[right]);
}
return left;
}
int QuickSort(int left, int right, int m)//m表示n-k,即要找的元素在有序序列中最终的下标
{
if(left < right)
{
int position = Partition(left, right);
if(position == m)
{
return position; // 枢轴元素最终位置,对应数组下标
}
else if(position < m)
{
return QuickSort(position + 1, right, m);
}
else{
return QuickSort(left, position - 1, m);
}
}//只要K比n小,就一定能够找到,不会找不到
}
int main()
{
int n;
int k;//找到第K大的数
scanf("%d%d", &n, &k);
for(int i=0; i<n; i++)
{
scanf("%d", &arr[i]);
}
int ret = QuickSort(0, n-1, n-k);
printf("第%d大的数为:%d\n", k, arr[ret]);
return 0;
}
posted @   paopaotangzu  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示