排序算法(持续更新...)

选择排序

思路

选择排序(升序)可以理解为,不断地从序列中选择最小的元素,将其依次从左到右放置。

  1. 记录第i个位置的元素,索引设为min,将其与i+1~n(包含)的元素不断比较;
  2. 如果有比第i个位置小的元素,那么记录其索引min,并将其与之后的元素比较;
  3. 比较结束,交换索引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;

注意

  1. 代码中有两个循环,第一个控制位置的变化,第二个控制从当前位置之后的元素中找到最小的;
  2. 操作数为 ( n − 1 ) + ( n − 2 ) + . . . + 1 (n-1)+(n-2)+...+1 (n1)+(n2)+...+1,复杂度为 O ( n 2 ) O(n^2) O(n2)

冒泡排序

思路

冒泡的过程就是从左到右不断将两个数比较、交换位置的过程。

  1. 第一个循环控制冒泡次数,为n-1次;
  2. 第二个循环控制比较次数,为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;
			}
		}
	}
}

快速排序

思路

排序算法的思想很简单,即在每个循环过程中:

  1. 找到基准数;
  2. 通过基准数将这个序列拆分为两个部分(比基准数大或小);
  3. 对拆分后的序列进行同样的操作;
  4. 合并。

代码

#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;
	}

注意

  1. 快速排序在平均情况下的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),最糟糕的情况下的时间复杂度为 O ( n ) O(n) O(n)
  2. 归并排序的时间复杂度也是 O ( n l o g n ) O(nlogn) O(nlogn),我们在日常中更多的用到了快速排序,原因有两点:首先,快速排序在大部分情况下都处于平均情况;其次,我们在用大O法表示时间复杂度的时候是忽视了常量的,比如说:循环输出n个值与循环输出n个值并暂停1s花费的时间显然是不一样的,但是大O表示法却无法体现。常量在一些情况下的是不能够忽视的,归并排序和快速排序就是一个很好的例子,快速排序在平均情况下的常量会比归并排序小得多;
  3. 快速排序的速度极度依赖于基准值的选择。已知调用栈的每一层用于划分的时间复杂度都是 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函数是基于快速排序实现的,仅适用于普通数组和部分类型的容器。需要具备以下条件:

  1. 容器支持的迭代器类型必须为随机访问迭代器。这意味着sort函数只对array,vector,deque三个容器提供支持。
  2. 如果对容器中指定区域的元素做默认升序排序,则元素类型必须支持<运算符;同样,如果选用标准库提供的其他排序规则,元素类型也必须支持该规则底层实现所用的比较运算符。
  3. 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而言,底层实际上是一个双向链表,支持的是双向迭代器,其和随机访问迭代器的区别主要在于不支持以下操作:

  1. iter[n]:通过索引来移动迭代器至iter+n位置处的元素;
  2. +、-、+=、-=(++,–还是支持的);
  3. =,<=,>,<(==还是支持的)。

计数排序

思路

以升序排序为例,统计每个数比序列中的多少数大,得到的结果就是其在顺序数列中的位置。

统计时,我们采用数对(a,b)的形式,如果 a ≤ b a\le b ab,那么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;
}

注意

  1. 计数排序不是基于比较的排序;
  2. 计数排序的时间复杂度为 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();
		}
	}
}

注意

  1. 桶排序的缺点在于数据对象必须是正整数且范围不能太大;
  2. 桶排序的优势在于速度比较快,时间复杂度为O(m+n),m为取出的次数,n为收集的次数;
  3. 桶排序的实用价值不大,只适用于基数排序的一个步骤;
  4. 桶排序中的桶最好是链表,因为不知道桶有多大。
  5. 书写时,注意终止条件,因为涉及到链的删除,所以会导致指针等的失效;
  6. 桶排序也是一个非比较排序;
  7. 把桶按队列的形式入队出队,这使得相同数的顺序不变,该方法称为稳定排序

基数排序

思路

基数排序是桶排序的扩展方法,首先用基数将每一个数分解,比如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);
	}
}

注意

  1. 基数排序的时间复杂度为 Θ ( n ) \Theta(n) Θ(n)
  2. 基数排序相对快排来说,快一些;
  3. 因为在这里,我们用了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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

右边是我女神

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值