简介
排序算法是《数据结构与算法》中的最基本算法之一,这是需要熟练掌握的基本知识。排序算法可以分为内部排序和外部排序,内部排序是数据在内存中进行排序;而外部排序是因为数据量非常的大,无法一次性加载完所有的数据到内存中,在排序的过程中需要访问外存,例如磁盘。常见的内部排序算法有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序。
各类算法的基本信息,如下表所示:
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n*n) | O(n) | O(n*n) | O(1) | 内排序 | 稳定 |
选择排序 | O(n*n) | O(n*n) | O(n*n) | O(1) | 内排序 | 不稳定 |
插入排序 | O(n*n) | O(n) | O(n*n) | O(1) | 内排序 | 稳定 |
希尔排序 | O(n^1.3) | O(n) | O(n*n) | O(1) | 内排序 | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 外排序 | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n*n) | O(log n) | 内排序 | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 内排序 | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 外排序 | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n*n) | O(n+k) | 外排序 | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 外排序 | 稳定 |
名词解释
- n:数据规模
- k:“桶”的个数
- 内排序:占用常数内存,不占用额外内存
- 外排序:占用额外内存
- 稳定性:排序后2个相等剪枝的顺序和排序之前它们的顺序相同
排序算法
冒泡排序
冒泡排序,是一种简单直观的排序算法。它是按照顺序依次进行两个元素的比较,若发现它们的顺序是相反的,则进行交换;而冒泡排序算法的终止条件是没有可交换顺序的元素。
冒泡算法,不能说人人都会吧,但它应该是人人都很熟悉的一种排序算法,往往是在算法的第一位。其实冒泡排序还有一种优化的算法,即立一个flag,当在一趟遍历元素中没有发生交换,则证明该序列已是有序。但这种改进相对于性能的提升是没有太大的作用。
当数据顺序是正序的时候,冒泡排序最快;当数据是反序的时候,冒泡排序是最差的。
void sort::bubble_sort(int *array, int &length){
/* *
* @step
* 1. 比较相邻的元素,若第一个比第二大,则交换它们两个;
* 2. 对每一对相邻的元素做同样的工作,从开始第一对到结尾的最后一对。这样将会得到最大的数,并且在最后的位置;
* 3. 如上所述,对所有的元素重复以上两步,且每次比较的长度减1;
* 4. 没有需要对比的数之后,冒泡排序结束。
* */
for (int i = 0; i < length -1; ++i ) {
int sort_lenght = length - 1 - i;
for (int j = 0; j < sort_lenght; ++j) {
if(array[j] > array[j+1]){
// 数据交换 方法1
/*int tmp = array[j+1];
array[j+1] = array[j];
array[j] = tmp;*/
// 数据交换 方法2
/*array[j+1] = array[j] + array[j+1];
array[j] = array[j+1] - array[j];
array[j+1] = array[j+1] - array[j];*/
// 数据交换 方法3 有0时,不可用
/*array[j] = array[j] ^ array[j+1];
array[j+1] = array[j] ^ array[j+1];
array[j] = array[j] ^ array[j+1];*/
// 数据交换 方法4 有0时,不可用
array[j] = array[j] * array[j+1];
array[j+1] = array[j] / array[j+1];
array[j] = array[j] / array[j+1];
}
}
}
}
选择排序
选择排序,是一种直观的排序算法之一;无论什么数据在选择排序算法中,其时间复杂度均为O(n*n)。建议在数据规模小的时候,使用选择排序。它的唯一的好处是不占用额外的内存空间。
void sort::select_sort(int *array, int &length){
/* *
* @step
* 1. 从首元素到最后元素找到最大元素,与最后的一个元素替换;
* 2. 再从剩余的元素中找到最大元素,放在次位元素的位置;
* 3. 重复2步骤,直至所有的元素排序完成。
* */
for (int i = length - 1; i >= 0; --i) {
int index = i;
for( int j = i - 1; j >= 0; --j){
if(array[j] > array[index]){
index = j;
}
}
if (i == index)
continue;
// 交换数据
array[i] = array[i] ^ array[index];
array[index] = array[i] ^ array[index];
array[i] = array[i] ^ array[index];
}
}
插入排序
相对于冒泡和选择排序,插入排序是更容易理解的,因为玩过扑克牌的人都会这招。它的原理是通过构建一个有序的序列,对于未排序的数据,在有序的序列中从前向后进行扫描,找到相应的位置进行插入。
void sort::insert_sort(int *array, int &length){
/* *
* @step
* 1. 将待排序的首元素看做有序序列;
* 2. 将第二个元素到最后一个元素,进行排序插入到有序序列中;
* 3. 最后一个元素插入结束,即排序结束。
* */
for (int i = 1; i < length; ++i) {
int current_value = array[i];
int j = i - 1;
// 有序排序,方法1
/*for (j = i - 1; j >= 0; --j){
if (array[j] > current_value){
array[j+1] = array[j];
} else {
break;
}
}*/
// 有序排序,方法2
while ( j >= 0 && array[j] > current_value) {
array[j+1] = array[j];
--j;
}
array[j+1] = current_value;
}
}
希尔排序
希尔排序,是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是插入排序的一种,它是简单插入排序经过改进后的一个更高效的版本,也称之为缩小增量排序,同时也突破O(n^2)的第一批算子。它与插入算子的不同之处在于,他会优先选择距离较远的元素进行比较。其基本思想为先将整个待排序的元素序列分割为若干子序列分别进行直接插入排序,待整个序列中的元素基本有序时,在对全体元素进行直接插入排序。
void sort::shell_sort(int *array, int &length){
/* *
* @step
* 1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
* 2. 按增量序列个数k,对齐进行k趟排序;
* 3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,
* 分别对子序列进行插入排序;
* 4. 当增量因子为1时,整个序列作为一个表来处理,其长度为整个序列的长度。
* */
int gap, j, i;
for (gap = length >> 1; gap > 0; gap >>= 1) {
for ( i = gap; i < length; ++i){
int tmp = array[i];
for ( j = i - gap; j >= 0 && array[j] > tmp; j -= gap) {
array[j + gap] = array[j];
}
array[j+gap] = tmp;
}
}
}
归并排序
归并排序,是一种以“分而治之”的思想(即,分治法Divide and Conquer)为基础的排序算法;其排序的实现方式有两种:
一、自上而下的递归(所有的递归方法均可以用迭代实现);
二、自上而下的迭代。
归并排序,与选择排序一样,不受输入数据的影像,但其表现要比选择排序好很多,因而其时间复杂度始终为O(n log n),不过需要付出一定的代价——使用额外的内存空间。
void sort::merge_sort(int *array, int &length){
/* *
* @step
* 1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
* 2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
* 3. 比较两个指针指向的元素,选择小的元素放入到合并空间,并移动指针指到下一位置;
* 4. 重复步骤3知道某一指针达到序列尾;
* 5. 将另一序列剩下的所有元素直接赋值到合并序列尾。
* */
int* array_a = array;
int* array_b = (int*)calloc(length, sizeof(int));
int seg, start;
for ( seg = 1; seg < length; seg += seg) {
for (start = 0; start < length; start += seg * 2) {
int left = start;
int middle = ((start + seg) < length)? (start +seg) : length;
int right = ((start + seg * 2) < length)? (start +seg * 2) : length;
int index = left;
int start_1 = left, end_1 = middle;
int start_2 = middle, end_2 = right;
while (start_1 < end_1 && start_2 < end_2)
array_b[index++] = (array_a[start_1] < array_a[start_2]) ? \
array_a[start_1++] : array_a[start_2++];
while (start_1 < end_1)
array_b[index++] = array_a[start_1++];
while (start_2 < end_2)
array_b[index++] = array_a[start_2++];
}
int *temp = array_a;
array_a = array_b;
array_b = temp;
}
if (array != array_a){
for (int i = 0; i < length; ++i)
array_b[i] = array_a[i];
array_b = array_a;
}
free(array_b);
}
快速排序
快速排序,和归并排序有相似之处,均是采用“分而治之”的思想。它是由东尼●霍尔搞出来的排序算法;不同的是快速排序,是将一组元素(group)分为两个子组(sub_groups)进行排序的;且从本质上看,快速排序是递归分治法。
快速排序,从名字上就能感受到,它存在的意义就是块;它是处理大数据最快的排序算法之一。虽然最坏情况下的时间复杂度为O(n^2),但是在大多数情况下都比平均时间复杂度为O(n log n)的排序算法表现要好;它比复杂度稳定等于O(n log n)的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序,足见其优秀的程度。
void sort::quick_sort(int *array, int &length){
/* *
* @step
* 1. 从队列中筛选出一个元素,为基准;
* 2. 重新对序列进行排序,将比基准数小的放在前面,将比基准数大的放在后面;
* 3. 递归地将小于基准值的元素的子数列和大于基准元素的字数列分贝进行排序;
* */
quick_sort_partition(array, 0, length - 1);
}
void sort::quick_sort_partition(int *array, int left, int right){
const int current_value = array[left];
int start = left + 1;
int end = right;
while (start < end){
// 计算出比index_left大的index_max,
while (start <= right && array[start] <= current_value)
++start;
// 计算出比index_left小的index_min,
while (end > left && array[end] > current_value)
--end;
// index_max, index_min的数据进行替换
if (start <= end){
int tmp = array[end];
array[end] = array[start];
array[start] = tmp;
}
}
if (end > left){
int tmp = array[left];
array[left] = array[end];
array[end] = tmp;
}
if (left < end - 1)
this->quick_sort_partition(array, left, end - 1);
if (end + 1 < right)
this->quick_sort_partition(array, end + 1, right);
}
堆排序
堆排序, 是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
大顶堆: 每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆: 每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列。
void sort::heap_sort(int *array, int &length){
/* *
* @step
* 1. 创建一个堆H[0...lenght-1]
* 2. 把堆首(最大值)和堆尾互换;
* 3. 把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置;
* 4. 重复步骤2,直到堆的尺寸为1。
* */
// lamda表达式
auto swap = [](int *a, int *b){
int tmp = *a;
*a = *b;
*b = tmp;
};
// lamda表达式
auto max_heapify = [](int* array, int start, int end){
// 建立父节点和子节点
int dad = start;
int son = dad * 2 + 1;
// 子节点在范围内作比较
while (son <= end) {
// 先比较两个子节点的大小,选择最大的
if (son + 1 <= end && array[son] < array[son + 1])
son++;
// 如果父节点大于子节点,代表调整完成跳出函数
if (array[dad] > array[son])
return;
// 否则交换父子内容再继续子节点和孙节点的比较
else {
int tmp = array[dad];
array[dad] = array[son];
array[son] = tmp;
dad = son;
son = dad * 2 + 1;
}
}
};
// 初始化,i从最后一个父节点开始调整;
for (int i = length / 2 - 1; i >= 0; i--)
max_heapify(array, i, length - 1);
// 现将第一个元素和已经排好的元素前一位做交换,再重新调整,直到排序完成;
for (int i = length - 1; i > 0; i--) {
swap(&array[0], &array[i]);
max_heapify(array, 0, i - 1);
}
}
计数排序
计数排序, 它的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
void sort::count_sort(int *array, int &length){
/* *
* @step
* 1. 找出待排序的数组中最大和最小的元素;
* 2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
* 3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
* 4. 反向填充目标数组:将每个元素i放在新数组的第C项,每放一个元素就将C减去1。
* */
int *sorted_array = (int*)calloc(length, sizeof(int));
int *count_array = (int*)calloc(length, sizeof(int));
int i, j, k;
for (k = 0; k < 100; k++)
count_array[k] = 0;
for (i = 0; i < length; i++)
count_array[array[i]]++;
for (k = 1; k < 100; k++)
count_array[k] += count_array[k - 1];
for (j = length; j > 0; j--)
sorted_array[--count_array[array[j - 1]]] =
array[j - 1];
memcpy(array, sorted_array, sizeof(int) * length);
free(sorted_array);
free(count_array);
}
桶排序
桶排序,是计数排序的升级版本;主要利用了函数的映射关系,高效与否的关键在于这个映射的确定。当输入的数据被均匀的分配到桶中时,桶排序是最快的;当数据数据被分配到一个桶中时,桶排序是最慢的。为了使桶排序更加的高效,需要做到下面的三点:
- 在额定空间充足的情况下,尽量增大桶的数量;
- 使用的映射函数能够将输入的N个数据均匀的分配到k个桶中。
- 在对桶中的元素进行排序时,选择哪一种比较排序算法对于性能的影像也是重要的。
void sort::bucket_sort(int *array, int &length){
/* *
* @step
* 1. 元素分布在每一个桶中;
* 2. 对每一个桶中的元素进行排序;
* */
auto insert_fun = [](list_node *head, int data)->list_node*{
list_node dummy_node;
auto *new_node = new list_node(data);
list_node *pre, *curr;
dummy_node.next_node_ = head;
pre = &dummy_node;
curr = head;
while (nullptr != curr && curr->data_ <= data) {
pre = curr;
curr = curr->next_node_;
}
new_node->next_node_ = curr;
pre->next_node_ = new_node;
return dummy_node.next_node_;
};
auto merge_fun = [](list_node *head1, list_node *head2)->list_node*{
list_node dummy_node;
list_node *dummy = &dummy_node;
while (nullptr != head1 && nullptr != head2) {
if (head1->data_ <= head2->data_){
dummy->next_node_ = head1;
head1 = head1->next_node_;
} else {
dummy->next_node_ = head2;
head2 = head2->next_node_;
}
dummy = dummy->next_node_;
}
if (nullptr != head1) dummy->next_node_ = head1;
if (nullptr != head2) dummy->next_node_ = head2;
return dummy_node.next_node_;
};
int BUCKET_NUM = 10;
std::vector<list_node*> buckets(BUCKET_NUM, (list_node*)(0));
for (int i = 0; i < length; ++i) {
int index = array[i]/BUCKET_NUM;
list_node *head = buckets.at(index);
buckets.at(index) = insert_fun(head, array[i]);
}
list_node *head = buckets.at(0);
for (int i = 1; i < BUCKET_NUM; ++i) {
head = merge_fun(head, buckets.at(i));
}
for (int i = 0; i < length; ++i){
array[i] = head->data_;
head = head->next_node_;
}
}
基数排序
基数排序,是一种非比较整数排序算法,其原理是将整数按位数切割成不同的数据字,然后按每个位数分别进行比较。由于整数也可以表达字符串(名字、日期)和特定格式的浮点数(定点数),所以基数排序也不是只能使用于整数。基数排序分为两种方式:MSD,从高位开始排序;LSD,从低位开始排序。
基数排序、桶排序、计数排序比较
计数排序:每个桶只能存储单一键值;
桶排序:每个桶存储一定范围的数值;
基数排序:根据键值的每位数字来分配桶。
void sort::radix_sort(int *array, int &length){
/* *
* @step
* 1. 根据键值的每位数字分配桶。
* */
auto maxbit_fun = [](int *array, int length)->int{
int max_data = array[0];
for (int i = 1; i < length; ++i) {
max_data = max_data < array[i] ? array[i] : max_data;
}
int d = 1, p = 10;
while (max_data >= p) {
max_data /= 10;
++d;
}
return d;
};
int d = maxbit_fun(array, length);
int *tmp = (int*)calloc(length, sizeof(int));
int *count = (int*)calloc(10, sizeof(int));
int count_ = 0, radix = 1;
for ( int i = 1; i <= d; i++) {
// 初始化计数器
for (int j = 0; j < 10; ++j)
count[j] = 0;
// 统计每个桶中的记录数
for (int j = 0; j < length; ++j) {
count_ = (array[j] / radix) % 10;
count[count_]++;
}
// 将tmp中的位置依次分配给每个桶
for (int j = 1; j < 10; ++j) {
count[j] = count[j - 1] + count[j];
}
// 将所有桶中记录依次收集到tmp中
for (int j = length -1; j >= 0; --j) {
count_ = (array[j] / radix) % 10;
tmp[count[count_] - 1] = array[j];
count[count_]--;
}
// 将临时数组的内容复制到array中
for (int j = 0; j < length; ++j){
array[j] = tmp[j];
}
radix = radix * 10;
}
free(tmp);
free(count);
}
其余代码
// sort.h
#pragma once
#include <iostream>
/* *
* @brief The class of sort.
* */
class sort {
public:
/* *
* @brief The class of list node, it's used to bucket sort.
* */
class list_node{
public:
explicit list_node(int i = 0):data_(i), next_node_(nullptr){
}
~list_node()= default;
list_node* next_node_;
int data_;
};
/* *
* @brief Construction of sort class.
* */
sort() = default;
/* *
* @brief DeConstruction of sort class.
* */
~sort() = default;
/* *
* @brief Sort type.
* */
enum SORT_TYPE_EN {
BUBBLE_SORT, ///< Bubble sort.
INSERT_SORT, ///< Insert sort.
SELECT_SORT, ///< Select sort.
QUICK_SORT, ///< Quick sort.
SHELL_SORT, ///< Shell sort.
HEAP_SORT, ///< Heap sort.
MERGE_SORT, ///< Merge sort.
BUCKET_SORT, ///< Bucket sort.
COUNT_SORT, ///< Count sort.
RADIX_SORT ///< Radix sort
};
void bubble_sort(int *array, int &length);
void insert_sort(int *array, int &length);
void select_sort(int *array, int &length);
void quick_sort(int *array, int &length);
void shell_sort(int *array, int &length);
void heap_sort(int *array, int &length);
void merge_sort(int *array, int &length);
void bucket_sort(int *array, int &length);
void count_sort(int *array, int &length);
void radix_sort(int *array, int &length);
private:
void quick_sort_partition(int *array, int left, int right);
}; //sort class
void sort_test(sort::SORT_TYPE_EN &type, int *array, int &length);
/*******************************************************************/
//sort.c
void sort_test(sort::SORT_TYPE_EN &type, int *array, int &length){
sort my_sort;
switch (type){
case sort::BUBBLE_SORT:
my_sort.bubble_sort(array, length);
break;
case sort::INSERT_SORT:
my_sort.insert_sort(array, length);
break;
case sort::SELECT_SORT:
my_sort.select_sort(array, length);
break;
case sort::SHELL_SORT:
my_sort.shell_sort(array, length);
break;
case sort::QUICK_SORT:
my_sort.quick_sort(array, length);
break;
case sort::HEAP_SORT:
my_sort.heap_sort(array, length);
break;
case sort::MERGE_SORT:
my_sort.merge_sort(array, length);
break;
case sort::BUCKET_SORT:
my_sort.bucket_sort(array, length);
break;
case sort::COUNT_SORT:
my_sort.count_sort(array, length);
break;
case sort::RADIX_SORT:
my_sort.radix_sort(array, length);
break;
default:
break;
};
}
/*******************************************************************/
// main function
int main(int argc, char** argv){
int array_length = 20;
int array[array_length] = {15, 9, 7, 89, 60, 45, 3, 48, 21, 23,
22, 18, 1, 90, 73, 92, 76, 20, 79, 76};
// set sort type
sort::SORT_TYPE_EN sort_type = sort::RADIX_SORT;
sort_test(sort_type, array, array_length);
// print sorted data
for (int i = 0; i < array_length; ++i){
std::cout << array[i] << " " ;
}
std::cout << std::endl;
return 0;
}