选择排序
思路
选择排序(升序)可以理解为,不断地从序列中选择最小的元素,将其依次从左到右放置。
- 记录第i个位置的元素,索引设为min,将其与i+1~n(包含)的元素不断比较;
- 如果有比第i个位置小的元素,那么记录其索引min,并将其与之后的元素比较;
- 比较结束,交换索引min和索引i,返回1。
代码
代码的流程为,首先通过随机数生成器生成100个数,然后再通过选择排序进行排序。
#include <iostream>
#include <vector>
#include <random>
using namespace std;
void selection_sort(vector<int>& seq);
vector<int> create_random_seq(vector<int> seq, int n);
int main() {
vector<int> seq;
seq = create_random_seq(seq, 100);
selection_sort(seq);
for (int i = 0; i < 100; ++i)
cout << seq[i] << " ";
return 0;
}
// 选择排序(升序)
void selection_sort(vector<int>& seq) {
for (int i = 0; i < seq.size(); ++i) {
int min = i;
for (int j = i + 1; j < seq.size(); ++j) {
if (seq[min] > seq[j])
min = j;
}
if (min != i) {
int temp = seq[i];
seq[i] = seq[min];
seq[min] = temp;
}
}
}
// 生成长度为n的序列
vector<int> create_random_seq(vector<int> seq,int n) {
default_random_engine e;
uniform_int_distribution<> u(1, 1000);
for (int i = 0; i < n; ++i)
seq.push_back(u(e));
return seq;
注意
- 代码中有两个循环,第一个控制位置的变化,第二个控制从当前位置之后的元素中找到最小的;
- 操作数为 ( n − 1 ) + ( n − 2 ) + . . . + 1 (n-1)+(n-2)+...+1 (n−1)+(n−2)+...+1,复杂度为 O ( n 2 ) O(n^2) O(n2)
冒泡排序
思路
冒泡的过程就是从左到右不断将两个数比较、交换位置的过程。
- 第一个循环控制冒泡次数,为n-1次;
- 第二个循环控制比较次数,为n-1-i次;
代码
首先生成均匀分布的100个随机double型数据,再进行冒泡排序。
#include <iostream>
#include <vector>
#include <random>
using namespace std;
void create_sequence(vector<double>& seq, int n);
void bubble_sort(vector<double>& seq);
int main() {
vector<double> seq;
create_sequence(seq,100);
bubble_sort(seq);
for (int i = 0; i < 100; ++i)
cout << seq[i] << " ";
return 0;
}
void create_sequence(vector<double>& seq, int n) {
default_random_engine e;
uniform_real_distribution<double> u(0, 100);
for (int i = 0; i < n; ++i)
seq.push_back(u(e));
}
// 冒泡排序(升序)
void bubble_sort(vector<double>& seq) {
for (int i = 0; i < seq.size() - 1; ++i) {
for (int j = 0; j < seq.size() - 1 - i; ++j) {
if (seq[j] > seq[j + 1]) {
double temp = seq[j + 1];
seq[j + 1] = seq[j];
seq[j] = temp;
}
}
}
}
快速排序
思路
排序算法的思想很简单,即在每个循环过程中:
- 找到基准数;
- 通过基准数将这个序列拆分为两个部分(比基准数大或小);
- 对拆分后的序列进行同样的操作;
- 合并。
代码
#include <iostream>
#include <vector>
#include <random>
using namespace std;
void create_seq(vector<int>& seq, int n);
vector<int> quick_sort(vector<int> seq);
int main() {
vector<int> seq;
create_seq(seq, 100);
seq = quick_sort(seq);
for (vector<int>::iterator b = seq.begin(); b != seq.end(); ++b)
cout << *b << " ";
}
void create_seq(vector<int>& seq, int n) {
default_random_engine e;
uniform_int_distribution<int> u(0, 1000);
for (int i = 0; i < n; ++i)
seq.push_back(u(e));
}
// 快速排序(升序)
vector<int> quick_sort(vector<int> seq) {
// 基线条件
if (seq.size() < 2)
return seq;
// 递归条件
else {
// 基准元素
int base_element = seq.back();
seq.pop_back();
// 划分数组
vector<int> seq_less, seq_more;
for (vector<int>::iterator b = seq.begin(); b != seq.end(); ++b) {
if (*b <= base_element)
seq_less.push_back(*b);
else
seq_more.push_back(*b);
}
// 递归
seq_less = quick_sort(seq_less);
seq_more = quick_sort(seq_more);
// 合并
if (seq_more.empty())
seq_more.insert(seq_more.begin(), base_element);
else
seq_less.push_back(base_element);
seq_less.insert(seq_less.end(), seq_more.begin(), seq_more.end());
return seq_less;
}
注意
- 快速排序在平均情况下的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),最糟糕的情况下的时间复杂度为 O ( n ) O(n) O(n);
- 归并排序的时间复杂度也是 O ( n l o g n ) O(nlogn) O(nlogn),我们在日常中更多的用到了快速排序,原因有两点:首先,快速排序在大部分情况下都处于平均情况;其次,我们在用大O法表示时间复杂度的时候是忽视了常量的,比如说:循环输出n个值与循环输出n个值并暂停1s花费的时间显然是不一样的,但是大O表示法却无法体现。常量在一些情况下的是不能够忽视的,归并排序和快速排序就是一个很好的例子,快速排序在平均情况下的常量会比归并排序小得多;
- 快速排序的速度极度依赖于基准值的选择。已知调用栈的每一层用于划分的时间复杂度都是 O ( n ) O(n) O(n),在平均情况或者最佳情况下,调用栈的层数为 O ( n l o g n ) O(nlogn) O(nlogn),所以有 O ( n ) ∗ O ( l o g n ) = O ( n l o g n ) O(n)*O(logn)=O(nlogn) O(n)∗O(logn)=O(nlogn)。
最后,在C++中的<algorithm>库中的sort函数是基于快速排序实现的,仅适用于普通数组和部分类型的容器。需要具备以下条件:
- 容器支持的迭代器类型必须为随机访问迭代器。这意味着sort函数只对
array,vector,deque
三个容器提供支持。 - 如果对容器中指定区域的元素做默认升序排序,则元素类型必须支持<运算符;同样,如果选用标准库提供的其他排序规则,元素类型也必须支持该规则底层实现所用的比较运算符。
- sort函数在实现排序时,需要交换容器中元素的存储位置。这种覃欢喜啊,如果容器中存储的是自定义的类对象,则该类的内部必须提供移动构造函数和移动赋值运算符。
需要注意的是,对于相同值,sort无法保证其位置不发生变动。
sort有2种用法,其语法格式为:
//对[first,last)区域内的元素做默认的升序排序
void sort(RandomAccessIterator first,RandomAccessIterator last);
// 按照指定规则comp进行排序
void sort(RandomAccessIterator first,RandomAccessIterator last,Compare comp);
// 其中comp可以是C++ STL标准库提供的排序规则(降序:std::greater<T>),也可以是自定义的排序规则
随机访问迭代器
在C++的大部分算法中,要求进行操作的容器支持随机访问迭代器。
那么如何区分迭代器呢?
在常见的顺序容器中,支持随机访问的即支持随机访问迭代器。
对于list而言,底层实际上是一个双向链表,支持的是双向迭代器,其和随机访问迭代器的区别主要在于不支持以下操作:
- iter[n]:通过索引来移动迭代器至iter+n位置处的元素;
- +、-、+=、-=(++,–还是支持的);
-
=,<=,>,<(==还是支持的)。
计数排序
思路
以升序排序为例,统计每个数比序列中的多少数大,得到的结果就是其在顺序数列中的位置。
统计时,我们采用数对(a,b)的形式,如果 a ≤ b a\le b a≤b,那么b所在的位置+1,这个等号用于处理相等的情况,于是每个数的索引就变成了 超过数的数量+左侧相同值。
代码
#include <iostream>
#include <vector>
using namespace std;
vector<int> count_sort(vector<int>& arr);
int main() {
vector<int> arr{ 5,3,87,2,9,2,10,30 };
vector<int> out = count_sort(arr);
for (auto& n : out)
cout << n << endl;
return 0;
}
vector<int> count_sort(vector<int> &arr) {
vector<int> rank(arr.size(), 0);
vector<int> seq(arr.size(), 0);
// 计数
for (int i = 1; i < arr.size(); i++) {
for (int j = 0; j < i; j++) {
if (arr[j] <= arr[i]) rank[i]++;
else rank[j]++;
}
}
// 排序
for (int i = 0; i < arr.size(); i++)
seq[rank[i]] = arr[i];
return seq;
}
注意
- 计数排序不是基于比较的排序;
- 计数排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2),但其在一定整数范围内,速度可达到 O ( n + k ) O(n+k) O(n+k),这是他的优势。
桶排序
思路
桶排序的思路就是先按照数的范围(0,range)设置range+1个桶,然后按照数的大小将其塞入桶n中。最后再依次取出即可。
代码
#include <iostream>
#include <list>
#include <vector>
#include "Student.h"
using namespace std;
using bin = vector<list<Student>>;
void bin_sort(list<Student>&, int);
int main() {
Student a("A", 9);
Student b("B", 7);
Student c("C", 8);
list<Student> chain{ a,b,c };
bin_sort(chain, 10);
for (auto& c : chain)
cout << c.score << endl;
return 0;
}
void bin_sort(list<Student> &chain,int range) {
bin bins;
bins.assign(range + 1, {});
// 收集
while(!chain.empty()) {
Student s = chain.front();
chain.pop_front();
bins[s.score].push_back(s);
}
// 取出
for (auto& b : bins) {
while (!b.empty()) {
chain.push_back(b.front());
b.pop_front();
}
}
}
注意
- 桶排序的缺点在于数据对象必须是正整数且范围不能太大;
- 桶排序的优势在于速度比较快,时间复杂度为O(m+n),m为取出的次数,n为收集的次数;
- 桶排序的实用价值不大,只适用于基数排序的一个步骤;
- 桶排序中的桶最好是链表,因为不知道桶有多大。
- 书写时,注意终止条件,因为涉及到链的删除,所以会导致指针等的失效;
- 桶排序也是一个非比较排序;
- 把桶按队列的形式入队出队,这使得相同数的顺序不变,该方法称为稳定排序。
基数排序
思路
基数排序是桶排序的扩展方法,首先用基数将每一个数分解,比如928可以通过基数10分解为数字9、2、8。
假设对0~999之间的10个整数进行排序,如果采用桶排序的话需要1000个桶,这样总共需要1000+1000+10,共2010步。
如果我们采用基数排序,用基数10对其进行分解,首先按照个位,对十个数进行桶排序;接着按照十位、百位对数进行桶排序。
因为箱子排序是稳定排序,所以说低位的顺序能得到保持。
这样的话,一共要进行103+103+10*3共90步。
对于一般的基数,相应的分解式 x % r i + 1 / r i x\%r^{i+1}/r^i x%ri+1/ri。
代码
#include<stack>
#include<list>
#include<vector>
#include<math.h>
#include<random>
#include<iostream>
using namespace std;
using bin = vector<stack<int,list<int>>>;
void bin_sort(vector<int>& seq, int range, int round, int radix);
void radix_sort(vector<int>& seq, int range);
int main() {
default_random_engine e;
uniform_int_distribution<int> u(0,999);
vector<int> seq;
for (int i = 0; i < 100; i++)
seq.push_back(u(e));
radix_sort(seq, 9);
for (auto& num : seq)
cout << num << endl;
return 0;
}
void bin_sort(vector<int>& seq, int range, int round,int radix) {
bin bins;
bins.assign(range + 1, { });
// 分配
while (!seq.empty()) {
bins[seq.back() % int(pow(radix, round + 1)) / int(pow(radix, round))].push(seq.back());
seq.pop_back();
}
// 收集
for (auto& b : bins) {
while (!b.empty()) {
seq.push_back(b.top());
b.pop();
}
}
}
void radix_sort(vector<int>& seq, int range) {
for (int i = 0; i < 3; i++) {
bin_sort(seq, range, i,10);
}
}
注意
- 基数排序的时间复杂度为 Θ ( n ) \Theta(n) Θ(n);
- 基数排序相对快排来说,快一些;
- 因为在这里,我们用了vector的结构,导致了只能从后面取出数据,所以说对应的桶应该是链表组成的栈结构。后面的数据先出来,后进去才能保证方法的稳定性。
堆排序
堆本身就是有一点顺序的,所以说堆可以用来实现n个元素的排序,所需的时间为 O ( n log n ) O(n\log n) O(nlogn)。
先用n个待排序元素来初始化一个大根堆,然后从堆中逐个提取(即删除)元素。初始化时间为 O ( n ) O(n) O(n),每次删除的时间为 O ( log n ) O(\log n) O(logn),因此总时间 O ( n log n ) O(n\log n) O(nlogn).
#include <iostream>
#include <queue>
#include <deque>
std::deque<int> heapSort(std::deque<int> &seq);
int main(int argc, const char * argv[]) {
std::deque<int> seq = {8,9,2,4,3,5,1};
seq = heapSort(seq);
for(auto& e:seq){std::cout<< e << std::endl;}
return 0;
}
std::deque<int> heapSort(std::deque<int> &seq){
std::priority_queue<int,std::deque<int>> q;
std::deque<int> new_seq;
for(auto& e:seq){q.push(e);}
for(int i = 0; i < seq.size(); i++){
new_seq.push_back(q.top());
q.pop();
}
return new_seq;
}