排序算法分为内部排序和外部排序,内部排序一般是在内存中实现,当数据量很大的时候,内存有限,不能将所有的数据都放到内存中来,这个时候就需要使用外部排序
记忆:
稳定:两个值相等的点排序前后顺序是否不变
- n: 代表数据规模及数据量大小
- k: 桶的个数
- In-place: 不占用额外内存,只占用常数内存
- Out-place: 占用额外内存
冒泡排序
遍历所有的数据,每次对相邻元素进行两两比较,如果顺序和预先规定的顺序不一致,则进行位置交换;这样一次遍历会将最大或最小的数据上浮到顶端,之后再重复同样的操作,直到所有的数据有序。
void bubble_sort(T arr[], int len) {
int i, j;
for (i = 0; i < len - 1; i++){
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1])
swap(arr[j], arr[j + 1]);
}
}
选择排序
先在数据中找出最大或最小的元素,放到序列的起始;然后再从余下的数据中继续寻找最大或最小的元素,依次放到排序序列中,直到所有数据样本排序完成。费时费力,无论什么数组都需要O(n^2)
void selection_sort(T arr[],int len) {
for(int i = 0;i < len;i++) {
int min = i;
for(int j = i + 1;j < len;j++) {
if(arr[j] < arr[min])
min = j;
}
std::swap(arr[i],arr[min]);
}
插入排序
它通过构建有序序列,对于未排序的数据序列,在已排序序列中从后向前扫描,找到相应的位置并插入,类似打扑克牌时的码牌。插入排序有一种优化的算法,可以进行拆半插入。
基本思路是先将待排序序列的第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;然后从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置,直到所有数据都完成排序;如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
void insertion_sort(T arr,int len){
for(int i=1;i<len;i++){
T key=arr[i];
int j;
for(j=i-1;j>=0 && key<arr[j];j--)
arr[j+1]=arr[j];
arr[j+1]=key;
}
}
希尔排序
希尔排序也称递减增量排序,是插入排序的一种改进版本,效率虽高,但它是一种不稳定的排序算法。
希尔排序在插入排序的基础上进行了改进,它的基本思路是先将整个数据序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全部数据进行依次直接插入排序。
假如有这样一组数据,[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果以步长5
进行分割,每一列为一组,那么这组数据应该首先分成这样
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
之后对每列进行插入排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
然后修改步长为3,最后为1
void shell_sort(T array[], int length) {
int h = 1;
while (h < length / 3) {
h = 3 * h + 1;
}
while (h >= 1) {
for (int i = h; i < length; i++) {
for (int j = i; j >= h && array[j] < array[j - h]; j -= h) {
std::swap(array[j], array[j - h]);
}
}
h = h / 3;
}
}
归并排序
采用分治法,它首先将数据样本拆分为两个子数据样本, 并分别对它们排序, 最后再将两个子数据样本合并在一起; 拆分后的两个子数据样本序列, 再继续递归的拆分为更小的子数据样本序列, 再分别进行排序, 直到最后数据序列为1,而不再拆分,此时即完成对数据样本的最终排序。
归并排序严格遵循从左到右或从右到左的顺序合并子数据序列, 它不会改变相同数据之间的相对顺序, 因此归并排序是一种稳定的排序算法.
作为一种典型的分而治之思想的算法应用,归并排序的实现分为两种方法:
- 自上而下的递归;
- 自下而上的迭代;
#include <iostream>
using namespace std;
int n;
const int N = 100000;
int q[N],temp[N];
void emerge_sort(int l,int r)
{
if (l >= r)
return;
int mid = (l + r) / 2;
emerge_sort(l, mid);
emerge_sort(mid + 1, r);
int i = l, j = mid + 1, k = 0; //i = left,细节
while (i <= mid && j <= r)
{
if (q[i] <= q[j])
temp[k++] = q[i++];
else
temp[k++] = q[j++];
}
while (i <= mid)
temp[k++] = q[i++];
while (j <= r)
temp[k++] = q[j++];
for (i = l, j = 0; i <= r; i++, j++) //重点!!!把排序好的写进数组里
q[i] = temp[j];
return;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> q[i];
}
emerge_sort(0, n - 1);
for (int i = 0; i < n; i++)
{
cout << q[i];
cout << " ";
}
return 0;
}
快速排序
快速排序,英文称为Quicksort,又称划分交换排序 partition-exchange sort 简称快排
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。首先从数列中挑出一个元素,并将这个元素称为「基准」,英文pivot。重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。之后,在子序列中继续重复这个方法,直到最后整个数据序列排序完成。
#include <iostream>
using namespace std;
int n;
const int N = 1e6 + 10;
int q[N];
void Quicksort(int q[], int l, int r)
{
if (l >= r)
return;
int x = q[(l - r) >> 1 + l]; //避免溢出
int i = l - 1;
int j = r + 1; //因为下面先do再while所以 + 1,但是大可不必
while (i < j)
{
do i++; while (q[i] < x);
do j--; while (q[j] > x);
if(i<j)
swap(q[i], q[j]);
}
Quicksort(q, l, j);
Quicksort(q, j + 1, r);
return;
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%d", &q[i]);
}
Quicksort(q, 0, n - 1);
for (int i = 0; i < n; i++)
{
printf("%d ", q[i]);
}
return 0;
}
堆排序
堆排序,英文称Heapsort,是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序实现分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
算法步骤:
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1
#include<iostream>
#include<algorithm>
#include<cstring>
#include<stack>
#include<unordered_map>
using namespace std;
const int N = 100010;
int m, n;
int h[N], siz;
void down(int u)
{
int t = u;
if (u * 2 <= siz && h[u * 2] < h[u]) t = u * 2;
if (u * 2 + 1 <= siz && h[t] > h[u * 2 + 1]) t = u * 2 + 1;
if (t != u)
{
swap(h[t], h[u]);
down(t);
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> h[i];
siz = n;
for (int i = n / 2; i >= 1; i--) down(i);
while (m--)
{
cout << h[1] << ' ';
h[1] = h[siz];
siz--;
down(1);
}
return 0;
}
计数排序
将输入的数据值转化为键存储在额外开辟的数组空间中。线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。比比较排序快,但是空间使用比较多
算法的步骤如下:
- (1)找出待排序的数组中最大和最小的元素
- (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
// 使用计数排序对输入数组进行排序
public static void CountSort(int[] arr) {
// 查找数组中的最大值和最小值
int max = arr[0];
int min = arr[0];
// 遍历数组以找到最大和最小值
for(int i = 0; i < arr.Length; i++) {
// 更新最大值
if (arr[i] > max) {
max = arr[i];
}
// 更新最小值
if (arr[i] < min) {
min = arr[i];
}
}
// 创建计数数组,长度为最大值和最小值之差加1
int[] count = new int[max - min + 1];
// 遍历数组并在计数数组中对应位置增加计数
for(int i = 0; i < arr.Length; i++) {
count[arr[i] - min]++;
}
// 重新排列原始数组
int index = 0;
for(int i = 0; i < count.Length; i++) {
// 根据计数数组中的计数,将元素回写到原始数组中
while (count[i]-- > 0) {
arr[index++] = i + min;
}
}
}
遍历最大最小值算是一个空间上的优化,否则如果数据集中在偏大的值附近(比如集中在 10010 -- 10020)就会造成很大的空间浪费
计数排序的一个重要性质是它是稳定的:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的相对次序是相同的。也就是说,对两个相同的数来说,在输入数组中先出现的数,在输出数组中也位于前面。
计数排序的稳定性很重要的一个原因是:计数排序经常会被用于基数排序算法的一个子过程。
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
1. 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
2. 什么时候最慢
当输入的数据被分配到了同一个桶中。
#include<iterator>
#include<iostream>
#include<vector>
using namespace std;
const int BUCKET_NUM = 10;
struct ListNode{
explicit ListNode(int i=0):mData(i),mNext(NULL){}
ListNode* mNext;
int mData;
};
ListNode* insert(ListNode* head,int val){
ListNode dummyNode;
ListNode *newNode = new ListNode(val);
ListNode *pre,*curr;
dummyNode.mNext = head;
pre = &dummyNode;
curr = head;
while(NULL!=curr && curr->mData<=val){
pre = curr;
curr = curr->mNext;
}
newNode->mNext = curr;
pre->mNext = newNode;
return dummyNode.mNext;
}
ListNode* Merge(ListNode *head1,ListNode *head2){
ListNode dummyNode;
ListNode *dummy = &dummyNode;
while(NULL!=head1 && NULL!=head2){
if(head1->mData <= head2->mData){
dummy->mNext = head1;
head1 = head1->mNext;
}else{
dummy->mNext = head2;
head2 = head2->mNext;
}
dummy = dummy->mNext;
}
if(NULL!=head1) dummy->mNext = head1;
if(NULL!=head2) dummy->mNext = head2;
return dummyNode.mNext;
}
void BucketSort(int n,int arr[]){
vector<ListNode*> buckets(BUCKET_NUM,(ListNode*)(0));
for(int i=0;i<n;++i){
int index = arr[i]/BUCKET_NUM;
ListNode *head = buckets.at(index);
buckets.at(index) = insert(head,arr[i]);
}
ListNode *head = buckets.at(0);
for(int i=1;i<BUCKET_NUM;++i){
head = Merge(head,buckets.at(i));
}
for(int i=0;i<n;++i){
arr[i] = head->mData;
head = head->mNext;
}
}
优化版:排序算法 | 桶排序算法原理及实现和优化_桶排序改进-CSDN博客
public class BucketSort03 {
public static void main(String[] args) {
int[] array = {50, 9, 1, 9, 53, 33, 27, 6, 1};// 待排序数组
sort(array);
print(array);
}
/** 从小到大排序 */
public static void sort(int[] array) {
// 确定元素的最值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < array.length; i++) {
max = Math.max(max, array[i]);
min = Math.min(min, array[i]);
}
// 桶数:(max - min) / array.length的结果为数组大小的倍数(最大倍数),以倍数作为桶数
int bucketNum = (max - min) / array.length + 1;
// 初始化桶
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
bucketArr.add(new ArrayList<Integer>());
}
// 将每个元素放入桶
for (int i = 0; i < array.length; i++) {
// 计算每个(array[i] - min)是数组大小的多少倍,看看放入哪个桶里
int num = (array[i] - min) / (array.length);
bucketArr.get(num).add(array[i]);
}
// 对每个桶进行排序
for (int i = 0; i < bucketArr.size(); i++) {
Collections.sort(bucketArr.get(i));
}
// 合并数据
int j = 0;
for (ArrayList<Integer> tempList : bucketArr) {
for (int i : tempList) {
array[j++] = i;
}
}
}
/** 打印数组 */
public static void print(int array[]) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
}
基于基数排序实现的桶排序:排序算法 | 基数排序算法原理及实现和优化_c 用基数排序算法,进行降序排序。-CSDN博客
public class BucketSort04 {
public static void main(String[] args) {
int[] array = {51, 944, 1, 9, 57, 366, 79, 6, 1, 345};// 待排序数组
sort(array);
System.out.println("最终排好序的数据:");
print(array);
}
/**
* 从小到大排序
*/
public static void sort(int data[]) {
int n = data.length;
// 使用数组来模拟链表(当然牺牲了部分的空间,但是操作却是简单了很多,稳定性也大大提高了)
// 十个桶。建立一个二维数组,行向量的下标0—9代表了10个桶,每个行形成的一维数组则是桶的空间
int bask[][] = new int[10][n];
// 用来计算每个桶使用的容量
int index[] = new int[10];
// 计算最大的数有多少位。比如:5978,有4位
int max = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int k = (data[i] + "").length();
max = max > k ? max : k;
}
String str;
// 循环内将所有数据补齐,长度都为 max 。第一轮 i 代表个位,第二轮 i 代表十位。。。
// 按照 个、十、百、千...的位置来计算
// 第一轮将10以内的数据排好序,第二轮将100以内的数据排好序......
for (int i = max - 1; i >= 0; i--) {
System.out.println("第" + (max - i) + "轮补齐后的数据:");
// 所有的数字都循环一遍
for (int j = 0; j < n; j++) {
str = "";
// 按照 max 将所有的数据补齐,位数不足的前面补零
if (Integer.toString(data[j]).length() < max) {
for (int k = 0; k < max - Integer.toString(data[j]).length(); k++)
str += "0";
}
str += Integer.toString(data[j]);
System.out.printf("%5s", str);
// index[str.charAt(i) - '0']用于第二层循环计算每个桶使用的容量,第二层循环结束后会将index[str.charAt(i) - '0']都初始化为零
// 第一轮取 str 的个位(str.charAt(i--)),放在第(str.charAt(i--) - '0')个桶的第(index[str.charAt(i) - '0']++)个位置
// 第二轮取 str 的十位(str.charAt(i--)),放在第(str.charAt(i--) - '0')个桶的第(index[str.charAt(i) - '0']++)个位置
// .......
bask[str.charAt(i) - '0'][index[str.charAt(i) - '0']++] = data[j];
}
// 将桶内的数据重新放入data数组内
int pos = 0;
for (int j = 0; j < 10; j++) {
// 第j个桶内有index[j]个数据
for (int k = 0; k < index[j]; k++) {
data[pos++] = bask[j][k];
}
}
System.out.println();
System.out.println("第" + (max - i) + "轮index内的数据:");
print(index);
System.out.println("第" + (max - i) + "轮桶内的数据:");
print(bask);
System.out.println("第" + (max - i) + "轮结束后data内的数据:");
print(data);
System.out.println();
// 将index[x]归零
for (int x = 0; x < 10; x++) index[x] = 0;
}
}
public static void print(int array[][]) {
for (int j = 0; j < array.length; j++) {
for (int k = 0; k < array[j].length; k++) {
System.out.printf("%5d", array[j][k]);
}
System.out.println();
}
}
public static void print(int array[]) {
for (int j = 0; j < array.length; j++) {
System.out.printf("%5d", array[j]);
}
System.out.println();
}
}
第一轮将10以内的数据排好序,第二轮将100以内的数据排好序…
第1轮补齐后的数据:
051 944 001 009 057 366 079 006 001 345
第1轮index内的数据:
0 3 0 0 1 1 2 1 0 2
第1轮桶内的数据:
0 0 0 0 0 0 0 0 0 0
51 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
944 0 0 0 0 0 0 0 0 0
345 0 0 0 0 0 0 0 0 0
366 6 0 0 0 0 0 0 0 0
57 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
9 79 0 0 0 0 0 0 0 0
第1轮结束后data内的数据:
51 1 1 944 345 366 6 57 9 79
第2轮补齐后的数据:
051 001 001 944 345 366 006 057 009 079
第2轮index内的数据:
4 0 0 0 2 2 1 1 0 0
第2轮桶内的数据:
1 1 6 9 0 0 0 0 0 0
51 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
944 345 0 0 0 0 0 0 0 0
51 57 0 0 0 0 0 0 0 0
366 6 0 0 0 0 0 0 0 0
79 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
9 79 0 0 0 0 0 0 0 0
第2轮结束后data内的数据:
1 1 6 9 944 345 51 57 366 79
第3轮补齐后的数据:
001 001 006 009 944 345 051 057 366 079
第3轮index内的数据:
7 0 0 2 0 0 0 0 0 1
第3轮桶内的数据:
1 1 6 9 51 57 79 0 0 0
51 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
345 366 0 0 0 0 0 0 0 0
944 345 0 0 0 0 0 0 0 0
51 57 0 0 0 0 0 0 0 0
366 6 0 0 0 0 0 0 0 0
79 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
944 79 0 0 0 0 0 0 0 0
第3轮结束后data内的数据:
1 1 6 9 51 57 79 345 366 944
最终排好序的数据:
1 1 6 9 51 57 79 345 366 944
有数组记录每一行有多少个有效数字,上面看着乱是因为没有把无效数字置零
桶排序的时间复杂度
对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M*(N/M)*log(N/M)) = O(N+N*(logN-logM)) = O(N+N*logN-N*logM)
当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。就变成计数排序
总结:桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。当然桶排序的空间复杂度为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
通过上面的性能分析,我们可以知道桶排序的特点,那就是速度快、简单,但是也有相应的弱点,那就是空间利用率低,如果数据跨度过大,则空间可能无法承受,或者说这些元素并不适合使用桶排序算法。
适用场景
桶排序的适用场景非常明了,那就是在数据分布相对比较均匀或者数据跨度范围并不是很大时,排序的速度还是相当快且简单的。
但是当数据跨度过大时,这个空间消耗就会很大;如果数值的范围特别大,那么对空间消耗的代价肯定也是不切实际的,所以这个算法还有一定的局限性。同样,由于时间复杂度为 O(n+m),如果 m 比 n 大太多,则从时间上来说,性能也并不是很好。
基数排序
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int maxData = data[0]; ///< 最大数
/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
/* int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;*/
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}