九大排序以及时间复杂度和稳定性分析

57 阅读5分钟

1. 直接插入排序

image.png

end指的是待插入元素前面的元素的下标,待查数组下标从1开始即可 时间空间复杂度O(N^2),空间复杂度O(1)

#include<cstdio>
#include<iostream>
#include<vector>using namespace std;
​
void InsertSort(int *arr, int n)
{
   for(int i = 0; i < n-1 ; ++i)
  {
       int end = i;
       int tmp = arr[end+1];//即将插入的元素
       while(end >= 0)
      {   //这里排升序,待插元素小的话往前走
           if(tmp < arr[end]){
               arr[end + 1] = arr[end];
               end--;
          }else{
               break;//找到要插入的位置
          }
      }
       arr[end + 1] = tmp;
  }
}
​
int main()
{
   int arr[10] = {1,3,5,7,2,4,8,6,4,3};
   for(int i = 0; i<10;++i)
  {
       cout << arr[i] << " ";
  }
   cout << endl;
   InsertSort(arr,10);
   for(int i = 0; i<10;++i)
  {
       cout << arr[i] << " ";
  }
   return 0;
}

2. 希尔排序

希尔排序,又称缩小增量法。其基本思想是:  1.先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作…  2.当增量的大小减到1时,就相当于整个序列被分到一组,进行一次直接插入排序,排序完成。

问题:为什么要让gap由大到小呢? answer:gap越大,数据挪动得越快;gap越小,数据挪动得越慢。前期让gap较大,可以让数据更快得移动到自己对应的位置附近,减少挪动次数。

void ShellSort(int *a, int n)
{
   //自己理解为以gap为组的直接插入排序
   int gap = n;
   while(gap>1)
  {
       gap/=2; //gap折半
       int i = 0;
       //进行间隔为gap的一趟排序
       //这里的下标为n-gap-1指的是最后一组gap的开头
       for(i = 0i< n - gap;++i)
      {
           int end = i;
           int tmp = a[end + gap];
           while(end >= 0)
          {
               if(tmp < a[end])
              {
                   a[end + gap] = a[end];
                   end-=gap;
              }
               else
              {
                   break;
              }
          }
           a[end + gap] = tmp;
      }
  }
}

3. 堆排序

学习堆排序,首先要学会建堆,建堆需要使用的是向下调整算法

向下调整算法(使用前提): 如果想要调整为小堆,那么左右子树必须都为小堆 如果想要调整为大堆,那么左右子树必须都为大堆

向下调整算法的基本思想(大堆为例):

  1. 从根节点开始,选出左右孩子中较大的孩子

  2. 让大的孩子和父节点比较 如果大的孩子比父亲大,那么孩子和父亲的位置交换,并且以原来大的孩子的位置当成父节点,继续向下调整,知道调整到叶子结点为止

    如果大的孩子比父亲小,则不需要处理了,调整已经完成了

image-20230406161442051

image.png 堆的向上调整算法 我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时候需要使用向上调整算法

基本思想(建大堆为例):

  1. 目标结点和父节点比较
  2. 如果目标结点大,就和父节点交换位置,并将目标结点的位置继续向上进行调整。 如果目标结点比父节点小,那就没有必要再进行交换了
#include<cstdio>
#include<iostream>
#include<assert.h>
​
using namespace std;
​
typedef int HPDataType;
​
typedef struct Heap
{
   HPDataType* a;//用于存储数据的数据
   int size;//记录堆中已有的元素数量
   int capacity;//记录堆堆容量
}HP;
​
//向下调整算法
//这里是大堆
void AdjustDown(int *a, int n, int root)
{
   int parent = root;
   int child = 2 * parent + 1;//先假设左孩子大(这边是下标0*2 + 1 = 1)
   while(child < n)
  {
       //右孩子存在并且比左孩子大
       if(child + 1 < n && a[child + 1] > a[child])
      {
           child++;
      }
       if(a[child] > a[parent])
      {
           swap(a[child],a[parent]);
           parent = child;
           child = 2 * parent + 1;
      }
       else //已经是堆了,不需要调整
      {
           break;
      }
  }
}
​
//向上调整算法
//大堆
void AdjustUp(int* a, int child)
{
   int parent = (child - 1)/2;
   while(child > 0)//等于0在根节点的位置没有必要调整
  {
       if(a[child] > a[parent])
      {
           swap(a[child],a[parent]);
           child = parent;
           parent = (child-1)/2;
      }
       else
      {
           break;
      }
  }
}
​
void HeapInit(HP* php, HPDataType* a, int n)
{
   assert(php);
   HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType)*n);
   if(tmp == nullptr)
  {
       cout << "malloc fail" << endl;
       exit(-1);
  }
   php->a = tmp;
   memcpy(php->a,a,sizeof(HPDataType)*n);
   php->size = n;
   php->capacity = n;
   int i = 0;
   //n-1 是右孩子(最后一个元素)的下标,n-1-1是倒数第二个也就是左孩子的下标
   //再除以2就可以找到他们的父亲
   for(int i = (n-1-1);i>=0;--i)
  {
       AdjustDown(php->a, php->size, i);
  }
}
​
void HeapPush(HP* php,HPDataType x)
{
   assert(php);
   if(php->size == php->capacity)
  {
       HPDataType* tmp = (HPDataType*)realloc(php->a,2 * php->capacity*sizeof(HPDataType));
       if(tmp == nullptr)
      {
           cout << "realloc failed" << endl;
           exit(-1);
      }
       php->a = tmp;
       php->capacity *= 2;
  }
​
   php->a[php->size] = x;
   php->size++;
   AdjustUp(php->a,php->size-1);
}
​
void HeapPop(HP* php)
{
   assert(php);
   
   swap(php->a[0],php->a[php->size-1]);
   php->size--;
   AdjustDown(php->a,php->size,0);
}
​
//销毁堆
void HeapDestroy(HP* php)
{
   assert(php);
   free(php->a);//释放动态开辟的内存
   php->a = nullptr;//即时置空
   php->capacity = 0;
   php->size = 0;
}
​
int main()
{
   HP* hp = (HP*)malloc(sizeof(HP));
   int a[10] = {3,4,7,1,2,42,12,23,45,6};
   HeapInit(hp,a,10);
   HeapPush(hp,30);
   for(int i = 0; i< hp->size;i++)
  {
       cout<< hp->a[i] << " ";
  }
   cout << endl;
​
   return 0;
}

4. 冒泡排序

这边的代码是从后往前冒泡 也就是说,一次得到得到一个序列中最小的值

void Bubble_Sort(int* a, int n)
{
   for(int end = n -1; end >= 0 ; --end)
  {
       int exchange = 0;//记录是否进行过交换,若没有进行过,说明数组已经有序了
       //从前往后冒,每次冒出一个最大值
       for(int i = 0; i< end;i++)
      {
           if(a[i]>a[i+1])
          {
               swap(a[i],a[i+1]);
               exchange = 1;
          }
      }
       if(exchange == 0)break;
  }
}

5. 快速排序

公认的排序之王,是Hoare提出的二叉树结构的 交换排序算法

基本思想: 任取待排元素中的某个元素作为基准值,按照该基准值将待排序列分成两个子序列,左子序都小于基准值,右子序都大于基准值,左右序列重复过程,直到所有元素都排列在对应位置上为止

三种分子序列方法:

  1. Hoare版本
  2. 挖坑法
  3. 前后指针法

5.1 Hoare版本:

  1. 选出一个key,一般是最左侧或者最右侧
  2. 定义两个指针l和r, l向右走,r向左走(注:若选择的是最左侧数据,需要R先走;若选择的是最右边数据,则L先走)
  3. 走的过程中,r遇到小于key的数停下,换L走,L遇到大于key的树,L和R内容互换,直到L和R最后相遇,此时相遇点的数和key交换即可

这样经过一次排序,key左边的数小于key, key右边的数大于key 终止条件是左右序列只有一个数或不存在

#include<iostream>
#include<cstdio>
#include<cmath>using namespace std;
​
//Hoare版的单次快排
int Hoare_QuickSort(int *a, int begin, int end)
{    
   int left = begin, right = end;
   int keyi = begin;//选最左边度数作为key
   while(left < right)
  {
       //right先走,找小
       while(left < right && a[right] >= a[keyi])
      {
           right--;
      }
       //left再走,找大
       while(left < right && a[left] <= a[keyi])
      {
           left++;
      }
       if( left < right )
      {
           swap(a[left],a[right]);
      }
  }
   int meeti = left;
   swap(a[keyi],a[meeti]);
   return meeti;
}
​
void QuickSort0(int* a,int begin, int end)
{
   if(begin >= end)return;
​
   int keyi = Hoare_QuickSort(a,begin,end);
   QuickSort0(a,begin,keyi-1);
   QuickSort0(a,keyi+1,end);
}
​
int main()
{
   int arr[10] = {1,3,5,2,9,12,4,2,8};
   QuickSort0(arr,0,9);
   for(int i = 0; i< 10; ++i)
  {
       cout << arr[i] << " ";
  }
   cout << endl;
   return 0;
}

5.2 挖坑法

//挖坑法
int Dig_QuickSort(int* a, int begin, int end)
{
   int l = begin, r = end;
   int key = a[l];
   while(l < r)
  {
       //右边先走,找到第一个小于key的数
       while(l < r && a[r] >= key)
      {
           --r;
      }
       //找到了小的,填坑
       a[l] = a[r];
​
       //左边走,找到第一个大于key的数
       while(l < r && a[r] <= key)
      {
           ++l;
      }
       //找到了,填坑
       a[r] = a[l];
  }
   //停下来,一直是l和r相遇了(在同一个地方)- 相遇点
   int meeti = l;
   a[meeti] = key;
   return meeti;
}

5.3 前后指针法

//前后指针法
void Pointer_QuickSort(int* a,int left,int right)
{
   int prev = left, cur = left + 1;
   int keyi = left;
   
   while(cur <= right)//cur没越界继续
  {
       if(a[cur] < a[keyi] && ++prev!=cur) //cur指向内容小于key时
           swap(a[prev],a[cur]);
       cur++;
  }
}

5.4 三数取中优化

//三数取中优化
//三数指的是最左,中间,最右
//取中指的是数的大小在中间的数
int GetMidIndex(int* a,int leftint right)
{
   int mid = left + (right - left)/2;
   if(a[mid] > a[right])
  {
       if(a[left> a[mid]) return mid;
       else if (a[right> a[left]) return right;
       else return left;
  }
   else
  {
       if(a[left< a[mid]) return mid;
       else if (a[left> a[right]) return right;
       else return mid;
  }
}

5.5 完整代码(内含非递归版本)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<stack>
​
using namespace std;
​
//Hoare版的单次快排
int Hoare_QuickSort(int *a, int begin, int end)
{    
   int left = begin, right = end;
   int keyi = begin;//选最左边度数作为key
   while(left < right)
  {
       //right先走,找小
       while(left < right && a[right] >= a[keyi])
      {
           right--;
      }
       //left再走,找大
       while(left < right && a[left] <= a[keyi])
      {
           left++;
      }
       if( left < right )
      {
           swap(a[left],a[right]);
      }
  }
   int meeti = left;
   swap(a[keyi],a[meeti]);
   return meeti;
}
​
//挖坑法
int Dig_QuickSort(int* a, int begin, int end)
{
   int l = begin, r = end;
   int key = a[l];
   while(l < r)
  {
       //右边先走,找到第一个小于key的数
       while(l < r && a[r] >= key)
      {
           --r;
      }
       //找到了小的,填坑
       a[l] = a[r];
​
       //左边走,找到第一个大于key的数
       while(l < r && a[r] <= key)
      {
           ++l;
      }
       //找到了,填坑
       a[r] = a[l];
  }
   //停下来,一直是l和r相遇了(在同一个地方)- 相遇点
   int meeti = l;
   a[meeti] = key;
   return meeti;
}
​
//前后指针法
void Pointer_QuickSort(int* a,int left,int right)
{
   int prev = left, cur = left + 1;
   int keyi = left;
   
   while(cur <= right)//cur没越界继续
  {
       if(a[cur] < a[keyi] && ++prev!=cur) //cur指向内容小于key时
           swap(a[prev],a[cur]);
       cur++;
  }
}
​
void QuickSort_Recursion(int* a,int begin, int end)
{
   if(begin >= end)return;
​
   //int keyi = Hoare_QuickSort(a,begin,end);
   int keyi = Dig_QuickSort(a,begin,end);
   QuickSort_Recursion(a,begin,keyi-1);
   QuickSort_Recursion(a,keyi+1,end);
}
​
void QuickSort_NOR(int* a,int begin, int end)
{
   stack<int> st;
   st.push(begin);
   st.push(end);
​
   while(!st.empty())
  {
       int right = st.top();
       st.pop();
       int left = st.top();
       st.pop();
​
       int keyi = Hoare_QuickSort(a,left,right);
       if(left < keyi - 1)
      {
           st.push(left);//左序列的l入栈
           st.push(keyi - 1);//左系列的r入栈
      }
       if(keyi + 1 < right)
      {
           st.push(keyi + 1);//右的L入栈
           st.push(right);// 右的R入栈
      }
  }
}
​
​
//三数取中优化
//三数指的是最左,中间,最右
//取中指的是数的大小在中间的数
int GetMidIndex(int* a,int left, int right)
{
   int mid = left + (right - left)/2;
   if(a[mid] > a[right])
  {
       if(a[left] > a[mid]) return mid;
       else if (a[right] > a[left]) return right;
       else return left;
  }
   else
  {
       if(a[left] < a[mid]) return mid;
       else if (a[left] > a[right]) return right;
       else return mid;
  }
}
​
int main()
{
   int arr[10] = {9,8,7,5,6,2,3,2,1,11};
   //QuickSort_Recursion(arr,0,9);
   QuickSort_NOR(arr,0,9);
   for(int i = 0; i< 10; ++i)
  {
       cout << arr[i] << " ";
  }
   cout << endl;
   return 0;
}

6. 归并排序

image-20230407162718877

image.png 归并排序,从思想上就很适合用递归来实现,用递归实现也比较简单,我们需要申请一个与待排序列大小相同的数据用于合并过程两个有序的子序列,合并完毕后再将数据拷贝回原数组

#include<iostream>
#include<cstdio>
​
using namespace std;
​
//归并排序的子函数
void _MergeSort(int* a, int left, int right, int* tmp)
{
   if(left >= right)
  {
       return;
  }
   int mid = left + (right - left)/2;
   _MergeSort(a, left, mid, tmp);
   _MergeSort(a, mid + 1, right, tmp);
   int begin1 = left, end1 = mid;
   int begin2 = mid + 1, end2 = right;
   //将两段子区间进行归并,归并的结果放在tmp中
   int i = left;
   while(begin1 <= end1 && begin2 <= end2)
  {
       if(a[begin1] < a[begin2])
      {
           tmp[i++] = a[begin1++];
      }
       else
      {
           tmp[i++] = a[begin2++];
      }
  }
​
   //遍历完了一个区间,还有一个直接放到tmp的后面即可
   while(begin1 <= end1) tmp[i++] = a[begin1++];
   while(begin2 <= end2) tmp[i++] = a[begin2++];
   
   //归并完了以后,拷贝回原来的数据
   for(int j = left; j<=right; ++j)
  {
       a[j] = tmp[j];
  }
}
​
//归并排序的函数主体
void MergeSort(int* a, int n)
{
   //申请和原数组大小相同的一片区域
   int* tmp = (int*)malloc(n * sizeof(int));
   if(tmp == nullptr)
  {
       cout << "malloc failed" << endl;
       exit(-1);
  }
   _MergeSort(a,0,n-1,tmp);
   free(tmp);
}
​
void _MergeSortNonR(int* a, int* tmp,int begin1, int end1, int begin2, int end2)
{
   int i = begin1;
   int j = begin1;
   while(begin1 <= end1 && begin2 <= end2)
  {
       //将较小的数放入tmp
       if(a[begin1] < a[begin2])
           tmp[i++] = a[begin1++];
       else
           tmp[i++] = a[begin2++];
  }
   //剩余余下的数据进行处理
   while(begin1 <= end1) tmp[i++] = a[begin1++];
   while(begin2 <= end2) tmp[i++] = a[begin2++];
​
   for(; j <= end2 ;j++)
  {
       a[j] = tmp[j];
  }
}
//归并排序(非递归写法)
void MergeSortNonR(int* a,int n)
{
   //生成一块空间,供数组进行拷贝
   int* tmp = (int*)malloc(n * sizeof(int));
   if(tmp == nullptr)
  {
       cout << "malloc failed" << endl;
       exit(-1);
  }
   int gap = 1;
   while(gap < n)
  {
       //两组两组进行归并
       for(int i = 0; i < n; i += 2 * gap)
      {
           int begin1 = i, end1 = i + gap-1;
           int begin2 = i + gap, end2 = i + 2 * gap -1;
           if(begin2 >= n)//最后一组的第二个小区间不存在 或者第一个小区间不够gap
               break;
           if(end2>=n) // 最后一组的第二个小区间不够gap个,则第二个小区间后界变成数据的后界
           end2 = n-1;
           _MergeSortNonR(a,tmp,begin1,end1,begin2,end2);
      }
       gap *= 2;
  }
}
​
​
int main()
{
   int a[10] = {1,3,5,2,4,6,9,8,3,12};
   //MergeSort(a,10);
   MergeSortNonR(a,10);
   for(int i = 0; i< 10;i++)
  {
       cout<< a[i] <<" ";
  }
   cout<<endl;
   return 0;
}

7. 选择排序

选择排序,即每次从待排序列中选出一个最小值,然后放在序列的起始位置,直到全部待排数据排完即可。

void SelectSort(int* a,int n)
{
   for(int i = 0; i< n;++i)
  {
       int start = i;//记录起始位置
       int mini = start; // 最小值的下标
       while(start < n)
      {
           if(a[mini] > a[start])mini = start;
           start++;
      }
       swap(a[i],a[mini]);
  }
}

优化:一次选两个数,这样可以使排序的效率快一倍

void SelectSor_Plus(int* a,int n)//在一轮选出最小数的基础上,把最大数也选出来
{
   int left = 0 , right = n-1;
   while(left < right)
  {
       int maxIndex = left, minIndex = right;
       for(int i = lefti <= right; ++i)
      {
           if(a[minIndex] > a[i]) minIndex = i;
           if(a[maxIndex] < a[i]) maxIndex = i;
      }
       swap(a[left],a[minIndex]);
       if(left == maxIndex)//避免最大值在开头,刚好被替换掉
      {
           maxIndex = minIndex;
      }
       swap(a[right],a[maxIndex]);
       left++,right--;
  }
}

8. 计数排序

计数排序又叫非比较排序,该算法不是通过比较数据排序,然后收集元素出现的次数 !

image.png

若使用计数排序,我们应该使用相对映射

#include<iostream>
#include<cstdio>
​
using namespace std;
​
void CountSort(int* a, int n)
{
   int min = a[0], max = a[0];
   for(int i = 0; i < n; ++i)
  {
       if(a[i]<min) min = a[i];
       if(a[i]>max) max = a[i];
  }
​
   int range = max - min + 1;
   //开辟空间 并且 置0
   int* count = (int*)calloc(range,sizeof(int));
   if(count == nullptr)
  {
       cout << "calloc failed" << endl;
       exit(-1);
  }
​
   for(int i = 0; i < n; ++i)
  {
       count[a[i] - min]++;
  }
   int i = 0;
   for(int j = 0; j < range;j++)
  {
       while(count[j]--)
      {
           a[i++] = j + min;
      }
  }
}
​
int main()
{
   int a[10] = {1,1,33,21,5,3,7,8,5,6};
   CountSort(a,10);
   for(int i = 0; i < 10; ++i)
  {
       cout << a[i] <<" ";
  }
   cout << endl;
   return 0;
}

9. 外部排序

内排序:数据量相对少一些,可以放到内存中进行排序 外排序:数据量较大,内存中存不下,只能放到磁盘文件中,需要排序

归并排序能够胜任海量数据的排序

假设选择有10亿个整数(4GB)存放在文件A中,需要我们进行排序,而内存一次只提供512MB空间 以下是基本思路:

  1. 每次从A中读1/8,也就是512MB,进行内排序,将排序结果写到一个文件中,然后再读1/8,最终就会有8个各自有序的小文件
  2. 对八个生成的小文件两两合并,最终8个合成4个,重复下去,最后变成一个有序文件

⚠️:这边的两两合并不是把两个文件读入内存进行合并,因为内存装不下,这边的是利用文件的输入输出函数,从两个文件中各读一个进行比较,将较小的写到新文件,不断比较写入,最后两个文件全都写到另第一个文件里去了

image.png

10. 时间复杂度分析和稳定性

image.png 稳定性分析博客

blog.csdn.net/Yeoman92/ar…

堆排序不稳定的例子

image.png