寒假训练营 第六节 基础算法(一)总结

排序算法

将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序

常见排序算法
快速排序、希尔排序、堆排序、直接选择排序不是稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。
分类
◆稳定排序:假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法是稳定的。其中冒泡,插入,基数,归并属于稳定排序,选择,快速,希尔,归属于不稳定排序。
◆就地排序:若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间为O(1),则称为就地排序。
在这里插入图片描述

计数排序

1、题目详情:洛谷 P1271 【深基9.例1】选举学生会

学校正在选举学生会成员,有 n ( n ≤ 999) 名候选人,每名候选人编号分别从 1 到 n ,现在收集到了 m ( m < 2000000) 张选票,每张选票都写了一个候选人编号。现在想把这些堆积如山的选票按照投票数字从小到大排序。输入 n 和 m 以及 m 个选票上的数字,求出排序后的选票编号。

#include<iostream>
using namespace std;
int a[1000]={0},n,m,tmp;
int main(){
	cin>>n>>m;
	for(int i=0;i<m;i++){
		cin>>tmp;
		a[tmp]++;
	}
	for(int i=1;i<=n;i++)
		for(int j=0;j<a[i];j++)
		cout<<i<<' ';
	cout<<endl;
	return 0;
}

计数排序 顾名思义,即统计每一个元素出现的次数,再按照顺序依次排列。数列中的元素就是“票”,而一个与元素取值范围相符的数组就是“票箱”。记n为数列长度, m为取值范围。需要O(n)的时间统计每一数值出现次数。之后再用O(n + m)的时间构造出结果数列,总时间O(n + m)
另外,需要O(m) 的额外空间作为票箱。

优点: 当m较小时,时间复杂度近似于O(n),性能强大。

缺点: 当m远大于n时,时空复杂度均取决于m,得不偿失。取值范围为非整数时,无法实现。

计数排序变种:

  • 离散化计数排序
    若能将不可表示的数据范围(双向)映射到较小的整数集合上,
    则可以在映射后使用计数排序。这一映射过程称为离散化。
  • 桶排序
    将数列按数值区间(而非具体数值)划分为若干个桶。桶内采用
    其他排序算法。
  • 基数排序
    从低到高对每一个(X进制)位进行一次计数排序。这样,当高位
    有序时,所有低位均已有序。可以保证只使用X个桶。

冒泡排序

思路:不超过n次大循环;每次大循环,按照顺序比较相邻元素并交换,直到序列有序。

for (int i = 0; i < n - 1; i++)
	for (int j = 0; j < n - i - 1; j++)
		if (a[j] > a[j + 1])
			swap(a[j], a[j + 1]);

特点:总交换次数恰为逆序对数。每次迭代都能保证至少一个(最大)元素的位置被确定。后 i 个元素有序,且为最大的 i 个元素。

#include <iostream>
#include <vector>

using namespace std;
/*冒泡排序*/

void bubbleSort(int* arr, int n);
int main() {
 	int a[100];
	int n;
	 cin >> n;
 	for (int i = 0; i < n; i++) {
 		cin >> a[i];
 	}
	 bubbleSort(a, n);
 	for (int i = 0; i < n; i++) {
 		cout << a[i] << " ";
 	}
 	return 0;
}

void bubbleSort(int* arr, int n){
 	int temp;
 	for (int i = 0; i < n - 1; i++) {  // n个数排序,只需冒泡 n-1 次

 	for (int j = 0; j < n - i - 1; j++) { 
 		if (arr[j] < arr[j + 1]) {
 			temp = arr[j];
 			arr[j] = arr[j + 1];
			 arr[j + 1] = temp;
 		}
	 }
   }
 }

在这里插入图片描述

选择排序

思路: n 次大循环,第 i 次大循环中,用小循环寻找数列中第 i 小的元素。如果发现更小的数字,则交换至第 i 个位置。

for (int i = 0; i < n - 1; i++)
	for (int j = i + 1; j < n; j++)
		if (a[j] < a[i])
			swap(a[i], a[j]);
//	事实上,由于前i-1项已经排序完毕,第i小的元素等价于第i至第n项中的最小元素。

特点:思路简单,实现更简单。
每次迭代都能保证至少一个(最小)元素的位置被确定。前i个元素有序,且为最小的i个元素。

#include <iostream>

using namespace std;

void selectSort(int* arr, int n);
int main() {
 	int a[100];
 	int n;
 	cin >> n;
 	for (int i = 0; i < n; i++) {
 		cin >> a[i];
 	}
 	selectSort(a, n);
 	for (int i = 0; i < n; i++) {
		 cout << a[i] << " ";
 	}
 	return 0;
}
void selectSort(int* arr, int n){
	int minIndex;
 	int temp;
	for (int i = 0; i < n; i++) {
 	minIndex = i;
 	for (int j = i; j < n; j++) {
 		if (arr[j] < arr[minIndex]) {
 			minIndex = j;
 		}
 	}
 	if (i != minIndex) {
 		temp = arr[i];
 		arr[i] = arr[minIndex];
 		arr[minIndex] = temp;
 	}
  }
}

在这里插入图片描述

插入排序

思路:n次大循环;第i次大循环将第i个元素向前交换,直至左侧元素不大于它,或抵达数列首部。

for (int i = 1; i < n; i++) {
	int now = a[i], j; // 记录一下待插牌,等下还要放回去
	for (int j = i - 1; j >= 0; j--)
	if (a[j] > now)
		a[j + 1] = a[j];
	else break;
		a[j + 1] = now;
}

特点:前 i 个元素有序。但是直到排序完成;不能保证任何一个元素的最终位置被确定(设想最小元素在数列尾部)。可以用来动态维护前 k 小元素,单次插入时间复杂度O(k) 。在此场景下,第 k + 1小的元素将不会右移,而是被直接丢弃。

#include <iostream>

using namespace std;

void InsertSort(int *arr, int len){
    int temp;
    for (int i = 1; i < len; i++) {
        
        temp = arr[i];
        if (arr[i] < arr[i - 1]) {
            temp = arr[i];
            int j = i - 1;
            for (; j >= 0 && temp < arr[j]; j--) {
                arr[j + 1] = arr[j];
           }
            arr[j + 1] = temp;
       }
   }
}

int main(){
    int a[100];
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
   }
    InsertSort(a, n);
    for (int i = 0; i < n; i++) {
        cout << a[i] << " ";
   }
    cout << endl;
    return 0;
}

在这里插入图片描述

快速排序

选择排序,每一迭代确定一个元素的位置。但是所有已确定元素均在左侧,对右侧没有指导意义。

插入排序,每一迭代只需要与大于自身的值比较,期望只需比较一半的元素。但是没有元素确定位置;新元素的插入导致所有更大的元素右移,带来额外开销。

考虑结合二者优点。以这个数列为例:3 8 4 10 6 7 2 5 9 1
尝试随机选取一个元素确认位置,称为哨兵数。
3 8 4 10 6 7 2 5 9 1
将序列中所有比哨兵数小的数字都移动到哨兵数的左边,所有比哨兵数大的数字都移动到哨兵数的右边。
3 4 2 5 1 6 8 10 7 9
显然哨兵数左右两侧的元素不再需要任何比较。因此,对两侧的子数列分别采用相同的做法,直到数列不可再分(长度为0或1)。
在这里插入图片描述
最优情况下,每次选择的哨兵数均将数列对半分开。则
O( log ⁡ 2 ( n ) \log_{2}(n) log2(n) )次划分后数列将不可再分。每次划分的复杂度为O(n),
总复杂度O( n log ⁡ 2 ( n ) n\log_{2}(n) nlog2(n) )。
最坏情况下,每次选择的哨兵数均在数列一端。则退化为选择
排序算法,总复杂度O( n 2 n^{2} n2)。

// 快排模板 并不存在一种在所有情况下都最优的实现,需根据应用场景选择合适的实现。
void qsort(int a[], int l, int r) { // 引入数组的地址
	int i = l, j = r, flag = a[(l + r) / 2], tmp; // flag=哨兵
	do {
		while (a[i] < flag) i++; // 从左找比哨兵大的数
		while (a[j] > flag) j--; // 从右找比哨兵小的数
		if (i <= j) { // 交换
			swap(a[i], a[j]);
			i++; j--;
		}
	} while (i <= j);

// 习惯上,上面用于分段的过程一般称作partition

	if (l < j) qsort(a, l, j);
	if (i < r) qsort(a, i, r);
}

快排是一种基于分治法的排序算法。
大多数基于分治法的算法均具有O(nlogn)的时间复杂度。
其余具有此复杂度的排序算法还有:

  • 基数排序,基于按位处理
  • 归并排序,基于分治法
  • 堆排序,基于数据结构
#include <iostream>

using namespace std;
// 一个完整的快速排序代码
void quickSort(int* arr, int left, int right);

int main() {
 	int a[100];
 	int n;
 	cin >> n;
 	for (int i = 0; i < n; i++) {
 		cin >> a[i];
 	} 
 	quickSort(a, 0,n);
 	for (int i = 0; i < n; i++) {
 		cout << a[i] << " ";
 	}
	cout << endl;
 	return 0;
}

void quickSort(int* arr, int L, int R)
{
 	if (L > R) return;
 	int left = L, int right = R;

 	int pivot = arr[left];
 	while (left < right){
 		while (left < right && arr[right] >= pivot) {
 			right--;
 		}
 		if (left < right) {
 			arr[left] = arr[right];
 		}
 		while (left < right && arr[left] <= pivot) {
 			right--;
 		}
 		if (left < right) {
 			arr[right] = arr[left];
 		}
 		if (left >= right) {
 			arr[left] = pivot;
 		}
 	}
 	quickSort(arr, L, right - 1);
 	quickSort(arr, right + 1, R);

}
void Quick_Sort(int *arr, int begin, int end){
    if(begin > end)
        return;
    int tmp = arr[begin];
    int i = begin;
    int j = end;
    while(i != j){
        while(arr[j] >= tmp && j > i)
            j--;
        while(arr[i] <= tmp && j > i)
 

            i++;
        if(j > i){
            int t = arr[i];
            arr[i] = arr[j];
            arr[j] = t;
       }
   }
    arr[begin] = arr[i];
    arr[i] = tmp;
    Quick_Sort(arr, begin, i-1);
    Quick_Sort(arr, i+1, end);
}
2、题目详情:洛谷 P1923 【深基9.例4】求第 k 小的数

输入 n ( n < 5000000 且 n 为奇数) 个数字 a i a_{i} ai ( a i a_{i} ai < 1 0 9 10^{9} 109) ,输出这些数字的第 k 小的数。最小的数是第 0 小。

#include<bits/stdc++.h>
using namespace std;
int a[5000005],n,k;
void qs(int left,int right)//双指针快速排序,还有一种是挖坑快速排序,数据结构的书本上就是后者
{
    if(left==right)return;
    int l=left,r=right,mid=l;
    while (l<r)
    {
        while(a[r]>=a[mid] && l<r)r--;
        while(a[l]<=a[mid] && l<r)l++;
        swap(a[l],a[r]);  
    }
    swap(a[mid],a[r]);
    if (k<l)//判断询问的位置,只要到询问的位置为整体有序即可
    {
        qs(left,l-1);
    }else if (k>l)
    {
        qs(l+1,right);
    }else
    {
        return;
    }
}
int main()
{
    cin >> n >>k;
    for (int i = 0; i < n; i++)
    {
        scanf("%d",&a[i]);//scanf比cin快,用cin会超时
    }
    qs(0,n-1);
    cout << a[k];
    
    return 0;
}
/*
如果 k 很小(常数级别),那么可以用选择排序直接把第 k 大的数选出来。然而并没有这个保证。
如果直接排序,我们已经知道可以在O(nlnn)的时间复杂度内使用快速排序求出。然而会超时
*/

STL中的排序算法

algorithm(算法)库包含很多常用的算法,包括排序。
包含头文件:#include <algorithm>
使用排序功能:

sort(a.begin(), a.end()); // O(nlogn)
sort(a.begin(), a.end(), cmp); // O(nlogn)

begin、end分别表示需要排序位置的首末。
cmp是一个可选参数,可以自定义排序的比较方法。
排序之后,可以使用以下算法:
去重:unique(a.begin(), a.end()); // O(n)
这个函数会返回去重后的序列末尾地址(序列长度可能会变短)。
查找:find(a.begin(), a.end(), val); // O(logn)
若元素存在则返回元素地址,否则返回末尾地址(end)。
对于数组和 vector(暂时没教) 的数组坐标:

  • a+0 = begin
  • a+n = end
  • a+(n-1) = 最后一个元素
  • end – begin = n
  • find(a[x]) – a = x
    在这里插入图片描述
sort(a.begin(), a.end());		// 对一个 vector 进行排序

sort(a.begin(), a.end(), cmp);	// 对一个 vector 进行【自定义】排序

sort(a, a+n);					// 对一个长度为 n 的数组 a 排序

sort(a, a+n, cmp);				// 对长度为 n 的数组 a【自定义】排序
3、题目详情:洛谷 P1059 [NOIP2006 普及组] 明明的随机数

给出 N ( N ≤ 100) 个 1 到 1000 的数字,输出去重后剩余数字的个数,以及去重排序后的序列。

  • 解法 1:
    注意到取值范围很小,可以使用计数排序。
  • 解法 2:
    直接使用STL中的排序、去重函数。
sort(a, a + n);
cnt = unique(a, a + n) - a;
cout << cnt << endl;
for (int i = 0; i < cnt; i++)
	cout << a[i] << ' ';

sort 函数可以增加第三个参数 cmp,也就是自定义排序的基准。
cmp 函数需要两个待排序的元素类型 a、b 作为参数。返回一个 bool 值,表示 x 是否严格小于 y。
例如:实现整数从大到小排序。sort(a, a + n, cmp);

bool cmp(int x, int y){
	return x > y;
}
/*
cmp 函数名称可以任取,保持上下一致即可。
*/

本题的AC代码:

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n,a[105],k=0;
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>a[i];
    sort(a,a+n);
    for(int i=0;i<n;i++)
        for(int j=i+1;j<n;j++)
            if(a[j]==a[i] && a[j]!=-1)
                a[j]=-1;
    for(int i=0;i<n;i++)
        if(a[i]!=-1)
            k++;
    cout<<k<<endl;
    for(int i=0;i<n;i++)
        if(a[i]!=-1)
            cout<<a[i]<<" ";
    return 0;
}

4、题目详情:洛谷 P1093 [NOIP2007 普及组] 奖学金

给出 n ( n ≤ 300) 名学生的语文、数学、英语成绩,这些学生的学号依次是从 1 到 n。需要对这些学生进行排序。如果总分相同,则语文分数高者名次靠前;如果语文成绩还是一样的,学号小者靠前。输出排名前 5 的学生学号和总分。
第 1 种 :使用 STL 进行排序
构造一个结构体 student 用户存储学生的各项有用的信息(数学和
英语并不重要,可以不用存下来)。注意使用 cmp 来进行比较,
当两个学生比较时,排名比较高的学生返回 true。

#include <algorithm>
#include <iostream>

using namespace std;

int const MAXN = 310;
int n;
struct student {
	int id, chinese, total;
}a[MAXN];
int cmp(student a, student b) {
	if(a.total != b.total) // 总分先定胜负
		return a.total > b.total;
	if(a.chinese != b.chinese) // 然后比语文
		return a.chinese > b.chinese;
	return a.id < b.id; // 最后比学号
}
int main() {
	cin >> n;
	for (int i = 0; i < n; i++) {
		int math, english;
		cin >> a[i].chinese>>math>>english;
		a[i].total=a[i].chinese+math+english;
		a[i].id = i + 1;
	}
	sort(a, a + n, cmp);
	for (int i = 0; i < 5; i++)
		cout<<a[i].id<<" "<<a[i].total<<endl;
	return 0;
}

第 2 种 :

#include <bits/stdc++.h>
using namespace std;
struct student {//典型结构体排序,总的来说比较简单,都是一个套路
	int math;
	int Chinese;
	int English;
	int num;
	int sum;
} p[100001];
bool cmp(student a,student b) {
    /*
    先按总分从高到低排序,
    如果两个同学总分相同,再按语文成绩从高到低排序,
    如果两个同学总分和语文成绩都相同,那么规定学号小的同学 排在前面,
    */
   if (a.sum!=b.sum)//总分不同
   {
        return a.sum>b.sum;
   }else if (a.Chinese!=b.Chinese)//总分相同,语文不同
   {
        return a.Chinese>b.Chinese;
   }else    //总分相同,语文相同,按学号排
   {
        return a.num<b.num;
   }

}
int main() {
	int n;
	cin>>n;
	for(int i=1; i<=n; i++) {
		p[i].num=i;
		cin>>p[i].Chinese>>p[i].math>>p[i].English;
		p[i].sum=p[i].Chinese+p[i].English+p[i].math;
	}
	sort(p+1,p+n+1,cmp);
	for(int i=1; i<=5; i++)//只要前五
		cout<<p[i].num<<" "<<p[i].sum<<endl;
	return 0;
}

除此之外还有选择排序和插入排序思路的其它解法,这里就不细说了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值