根据一篇博客(链接于末尾引用)的介绍,将各个排序算法,进行一遍实现,并能用自己的语言解释一番。
基础概念
稳定与不稳定,如果两个相等的数字,排序完成后,原来在前面的还是在前面,则是稳定的,否则为不稳定。
时间复杂度,如果某一种类型的排序,只需要一个循环就搞定,那么他的时间复杂度为,需要循环中再套循环,两个循环次数相同的时间复杂度为,次数不同为。
空间复杂度与时间复杂度类似,只是执行次数,变成了需要的空间大小。
数据准备
使用Python打印出来9999个数据后,直接复制粘贴进去,进行测试。
import random
def func():
nums = []
for i in xrange(0, 9999):
nums.append(str(random.randint(-99999, 99999)))
s = ','.join(nums)
print s
测试代码
#pragma once
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
class MySort {
public:
MySort() {}
~MySort() {}
void bubble(vector<int> &);
void selection(vector<int> &);
void insertion(vector<int> &);
void shell(vector<int> &);
void merge(vector<int> &);
void quick(vector<int>&, int, int);
void heap(vector<int> &);
void test();
private:
void swap(int &a, int &b);
int oneQuick(vector<int>&, int, int);
void createHeap(vector<int> &, int);
void adjustHeap(vector<int> &, int, int);
};
void MySort::swap(int &a, int &b) {
if (a >= b) return;
int tmp = a;
a = b;
b = tmp;
}
void MySort::test() {
// 数字为上面Python生成结果复制进来,太长就不复制进行了,input与input1的内容一样
vector<int> input = {};
clock_t start1 = clock();
sort(input.begin(), input.end());
clock_t end1 = clock();
cout << "STL use time: " << (double)(end1 - start1) / CLOCKS_PER_SEC << "s" << endl;
// 对于不同排序函数的调用,C++语法的函数指针,应该是可以做到的,此处先不用这个,直接改代码测试
vector<int> input1 = {};
clock_t start = clock();
// bubble(input1);
// ...
selection(input1);
clock_t end = clock();
cout << "Bubble use time: " << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl;
// 写归并排序的时候,发现应该检测一下我的排序是否正确,于是和STL的对比下
bool isRight = true;
for (size_t i = 0; i < input.size(); ++i) {
if (input[i] != input1[i]) {
isRight = false;
break;
}
}
cout << "Is my sort right: " << isRight << endl;
}
1、冒泡排序
类似于冒泡泡,将最大的那个数,一步步换到最后面去。维基百科上面,对冒泡排序的说法是:
由于它的简洁,冒泡排序通常被用来对于程序设计入门的学生介绍算法的概念。
我就通过复习冒泡,再次入门吧。冒泡排序的实现:
void MySort::bubble(vector<int> &input) {
for (size_t i = 0; i < input.size(); ++i) {
for (size_t j = i + 1; j < input.size(); ++j) {
swap(input[i], input[j]);
}
}
}
执行耗时:
STL use time: 0.013s
Bubble use time: 27.46s
可以看出来,这个差距不是一般的大!当数据更多的时候,肯定更加几何倍增加。
2、选择排序
感觉跟冒泡排序思路是一样的,区别在于选择排序,进行的交换少很多。
void MySort::selection(vector<int> &input) {
for (size_t i = 0; i < input.size(); ++i) {
size_t minIndex = i;
for (size_t j = i + 1; j < input.size(); ++j) {
if (input[j] < input[i]) {
minIndex = j;
}
}
swap(input[i], input[minIndex]);
}
}
执行耗时:
STL use time: 0.014s
Selection use time: 24.902s
从结果来看,确实比冒泡排序快些。
3、插入排序
将自己的理解记一下:
- 从第1个数字开始,假装当前已经有序。
- 第2个位置上的数字,如果第1个数字小,则将其插入到前面去。
- 第3个数字和之前有序序列的末尾比较,如果比第2个数字小,将其插入到第2个数字前面。如果还比第1个数字小,则再插入到更前面。
- 第4到n个数字,执行前面的步骤。
下面的代码,与博客中的代码进行了对比,发现他的速度比我的更快,对比原因直接写在注释。
void MySort::insertion(vector<int> &input) {
for (size_t i = 1; i < input.size(); ++i) {
for (size_t j = i; j > 0; --j) {
if (input[j] >= input[j - 1]) {
break;
}
else {
swap(input[j - 1], input[j]);
// 此处,我将swap换进来,用时为:27.843s
// 为什么会慢的原因,还没想清楚。我认为少了一次函数调用,应该会更快才对的???
// int tmp = input[j - 1];
// input[j - 1] = input[j];
// input[j] = tmp;
}
}
// 博客中的写法,用时为:14.181s
// 比我速度快的原因在于,这里的插入,一次只进行了一次赋值。
// 而我的版本,直接进行了交换,有3次赋值操作,导致速度差异
//int preIndex = i - 1;
//int current = input[i];
//while (preIndex >= 0 && input[preIndex] > current) {
// input[preIndex + 1] = input[preIndex];
// --preIndex;
//}
//input[preIndex + 1] = current;
}
}
我的版本执行用时:
STL use time: 0.017s
Insertion use time: 19.531s
可以看到,插入排序,速度比选择排序会快一点。
4、希尔排序
希尔排序,在我自己将整个算法写出来后,得出的结论是,这个排序算法,其实就是在插入排序外面又套一层循环。每个循环都做次插入排序,只是每次插入排序的数据,都只是原始输入的一部分。
// 在最开始写这个的时候,竟然忘掉了插入排序怎么做的。直接一层循环在那里换,囧。
void MySort::shell(vector<int> &input) {
vector<int> seq = { 16, 10, 7, 3, 1 };
// 保证seq.size()趟排序
for (int interval : seq) {
// 挨个的换跨度为interval的数字
for (int intervalIndex = 0; intervalIndex < interval; ++intervalIndex) {
// shell排序为简单插入排序改进版,所以必定需要包含一个简单插入排序的
for (size_t i = intervalIndex + interval; i < input.size(); i += interval) {
int preIndex = i - interval;
int curVal = input[i];
while (preIndex >= 0 && input[preIndex] > curVal) {
input[preIndex + interval] = input[preIndex];
preIndex -= interval;
}
input[preIndex + interval] = curVal;
}
}
}
}
执行用时:
STL use time: 0.014s
shell use time: 1.137s
看到这个执行用时,我被惊讶到了,瞬间快了好多好多倍。突破后的时间复杂度,在代码中的体现,简直太明显了。按照博客所说,希尔排序的时间复杂度为。
不过我重新搜索了一下,希尔排序的时间复杂度跟选择的有关系。复习先看维基百科吧。
5、归并排序(Merge Sort)
此次实现,没有看别人的解答,全是自己慢慢的磨。从昨天下午(2020-6-21)的不想做题,到鼓励自己花半小时试试看,从而投入一小时写出骨架,被合并操作卡住。晚上睡觉之前灵光一闪,知道合并问题所在,今早将其处理掉,得出正确解答。
记录下来后,和维基百科上面迭代版进行了对比,发现框架基本上一致。至于递归版,先暂时不考虑。
不过还有一个待处理的问题,当数据量太大,内存不够的时候,分治法的归并,到底是如何将数据合起来的呢?还没想通,这个需要想通。
看到一篇介绍分治法的博客里面一段话,忽然意识到,使用归并排序处理大数据,当数据拆分小块并排好序后的合并,只需要每次从每个小块中拿一个数字出来,进行比较,把最小的数字存到硬盘。这就好使了,也就是归并排序的一个步骤。
引用一下让我茅塞顿开的原文:
这里我们的做法是每次读取待排序文件的10000个数据,把这10000个数据进行快速排序,再写到一个小文件bigdata.part.i.sorted中。这样我们就得到了50000个已排序好的小文件了。
在有已排序小文件的基础上,我只要每次拿到这些文件中当前位置的最小值就OK了。再把这些值依次写入bigdata.sorted中。
void MySort::merge(vector<int> &input) {
vector<int> tmp1;
for (int step = 1; step < input.size(); step *= 2) {
for (size_t i = 0; i < input.size(); i += (2 * step)) {
tmp1.clear();
if (step == 1) {
// 最开始,2个数字一组,将小的放前面
if (i + 1 < input.size()) {
swap(input[i], input[i + 1]);
}
}
else {
// 再将已经排过序的2组2个数字,合并为1组4个数字;2组4个数字,合并为1组8个数字;...
int indexL = i;
int indexR = i + step;
for (indexL; indexL < i + step && indexL < input.size(); ++indexL) {
while (indexR < i + 2 * step && indexR < input.size() && input[indexR] < input[indexL]) {
tmp1.push_back(input[indexR]);
++indexR;
}
tmp1.push_back(input[indexL]);
}
// 卡的瓶颈在这里,因为我下面的while还加了一个限制条件:input[indexR] < input[indexL])
// 正确应该是当Left位置已经走完,只需要将right边的数字全扔进tmp就好
while (indexR < i + 2 * step && indexR < input.size()) {
tmp1.push_back(input[indexR]);
++indexR;
}
for (int j = i; j < i + 2 * step && j < input.size(); ++j) {
input[j] = tmp1[j - i];
}
}
}
}
}
执行用时:
STL use time: 0.013s
Bubble use time: 0.337s
Is my sort right: 1
可以可以,速度又提升了不少。
6、快速排序
在理解了基本算法思想之后,竟然在实现上有了卡顿。另一篇博客对于如何将大小数据进行挪动,有很形象的gif图说明。
主要的实现卡点为递归处,对于递归的理解,依然不到位。不不不!!!对于将数字进行左右分区的实现,也是不对的。
快速排序的基本规则为,和归并排序一样,是分治法的应用。
- 选一个数,使用这个数字,将整个待排序数列分成两部分,比这个数字小的放到左边,比这个数字大的数字放到右边。
- 从左边部分中再选一个数字,将左边部分分成两部分;对右边部分执行相同操作。
- 结束条件为,拆到最后没得拆了。如比3小的2和1是为左边部分,以2为拆分点,将比2小的1放到2的位置上,往右移,到结尾,将2放在最右边(即1)的位置上。
快速排序断断续续,分了3个时间段,各个时间段差不多花1小时,直到2020-6-29将它写出来,整个代码都算是抄的。遇到的那些问题,其实都是我的实现有问题。
void MySort::quick(vector<int>& input, int left, int right) {
if (left < right) {
int middle = oneQuick(input, left, right);
quick(input, left, middle - 1);
quick(input, middle + 1, right);
}
}
int MySort::oneQuick(vector<int>& input, int left, int right) {
int pivot = input[left];
while (left < right) {
while (left < right && input[right] >= pivot) {
right--;
}
if (left < right) {
input[left] = input[right];
left += 1;
}
while (left < right && input[left] <= pivot) {
left++;
}
if (left < right) {
input[right] = input[left];
right -= 1;
}
}
input[left] = pivot;
return left;
}
执行用时:
STL use time: 0.018s
Bubble use time: 0.007s
Is my sort right: 1
竟然比STL的还快!真的是快速排序了。
换回原来的电脑后,执行用时有了变化:
STL use time: 0.014s
Bubble use time: 0.051s
Is my sort right: 1
7、堆排序
懂得堆排序的大体框架,对于递归,我还是不甚理解。
由堆的性质决定,堆是一棵二叉树,且父节点必定大于(或小于)子节点的值,存放在一个数组中。当构建堆成功后,第一个元素则肯定为最大(最小)值。
构建堆后,将最大值放在最后一个位置,重新对堆进行处理,再将最大值放在倒数第二个位置。直到整个堆进行了调整。
void MySort::adjustHeap(vector<int>& input, int size, int index) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int largest = index;
if (left <= size - 1 && input[left] > input[largest]) {
largest = left;
}
if (right <= size - 1 && input[right] > input[largest]) {
largest = right;
}
if (largest == index) {
return;
}
else {
swap(input[largest], input[index]);
adjustHeap(input, size, largest);
}
}
void MySort::createHeap(vector<int>& input, int index) {
for (int i = (index - 1) / 2; i >= 0; --i) {
adjustHeap(input, index, i);
}
}
void MySort::heap(vector<int>& input) {
createHeap(input, input.size());
for (int i = input.size(); i > 0; --i) {
swap(input[0], input[i - 1]);
adjustHeap(input, i - 1, 0);
}
}
执行用时,还是挺快的。比归并快些,比快排慢。
STL use time: 0.013s
Bubble use time: 0.135s
Is my sort right: 1
至此,比较类排序全部搞完。
8、计数排序
在知道数字区间的时候,可以通过一个额外的数组,将每个数字出现的次数保存下来,然后再根据数字大小进行循环,将数字重新写入输入数组中。
我的实现版本:
#include <unordered_map>
void MySort::counting(vector<int>& input) {
unordered_map<int, int> tmp;
unordered_map<int, int>::iterator iter;
for (size_t i = 0; i < input.size(); ++i) {
iter = tmp.find(input[i]);
if (iter != tmp.end()) {
iter->second += 1;
}
else {
tmp[input[i]] = 1;
}
}
size_t index = 0;
for (int i = -100000; i < 100000; ++i) {
iter = tmp.find(i);
if (iter != tmp.end()) {
for (int j = 0; j < iter->second; ++j) {
input[index] = iter->first;
++index;
}
}
if (index == input.size()) {
break;
}
}
}
执行用时:
STL use time: 0.014s
Bubble use time: 0.916s
Is my sort right: 1
感觉应该是实现有问题吧。怎么会这么慢?对的,实现有问题,优化一把,不用unordered_map的版本。
#include <string.h>
void MySort::counting(vector<int>& input) {
int* tmpArr = new int[200000];
if (tmpArr == NULL) {
return;
}
else {
memset(tmpArr, 0, sizeof(int) * 200000);
}
for (size_t i = 0; i < input.size(); ++i) {
int* val = tmpArr + input[i] + 100000;
(*val)++;
}
size_t index = 0;
for (int i = -100000; i < 100000; ++i) {
int count = *(tmpArr + i + 100000);
while (count > 0) {
input[index] = i;
index++;
count--;
}
if (index >= input.size()) {
break;
}
}
delete[] tmpArr;
}
速度快了些。不止快了些,比快速排序还快了呢。
STL use time: 0.013s
Bubble use time: 0.029s
Is my sort right: 1
9、引用链接
- 那篇超级厉害的博客:www.cnblogs.com/onepixel/p/…
- 维基百科希尔排序:zh.wikipedia.org/wiki/%E5%B8…
- 分治法的介绍:blog.csdn.net/lemon_tree1…
- 快速排序的介绍:zhuanlan.zhihu.com/p/93129029