排序总结(difficult but Liver)(下)

简介: 排序总结(difficult but Liver)

②快速排序三数取中版

我们选数要选一个合适的数,不能过大也不能过小,大小适中最为适合,于是我们提出了三数取中,即:取第一个数,最后一个数以及中间的数三个数比较,取中位数。


int GetMidIndex(int* a, int begin, int end)
{
  int mid = begin + (end - begin) / 2;
  if (a[begin] < a[mid])
  {
  if (a[mid] < a[end])
    return mid;
  else//a[mid]>=a[end]
  {
    return a[end] > a[mid] ? end : mid;
  }
  }
  else//(a[begin]>=a[mid])
  {
  if (a[mid] > a[end])
    return mid;
  else//a[mid]<=a[end]
    return a[begin] > a[end] ? end : begin;
  }
}
int PartSort2(int* a, int left, int right)
{
  int mid = GetMidIndex(a,left,right);
  Swap(&a[left], &a[mid]);
  int keyi = left;
  while (left < right)
  {
  //因为选取在左侧,所以要right先走
  while (left < right && a[right] >= a[keyi])//右选小
  {
    right--;
  }
  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 QuickSort(int* a, int left, int right)
{
  if (left >= right)
  return;
  int meeti = PartSort2(a, left, right);
  QuickSort(a, left, meeti-1);
  QuickSort(a, meeti + 1, right);
}

1669253713439.jpg


三数取中有个细节需要提醒一下,如果找到中位数,不是把这个中位数的下标作为keyi,而是把这个数和最左侧的数进行交换,还是以left作为keyi。


我们再来看一下三数取中模式递归调用堆栈的情况。


1669253724340.jpg


我们看到三数取中已经很不错了,但是它最后三层调用了80%的堆栈,如果这样的快排去排更多的数据,很可能会栈溢出,为了避免,我们可以进一步优化,把最后接近有序的数据用插排来排,这样会快上不少,也能避免栈溢出。


void QuickSort(int* a, int left, int right)
{
  if (left >= right)
  return;
  if (right - left <= 8)
  {
  InsertSort(a + left, right - left + 1);
  }
  else
  {
  int meeti = PartSort2(a, left, right);
  QuickSort(a, left, meeti - 1);
  QuickSort(a, meeti + 1, right);
  }
}

1669253740865.jpg


2.挖坑法


1669253750124.jpg

挖坑法是快排的另一种玩法,效率和hoare版本的差不多,也是用到了三数取中。


//挖坑法
int PartSort3(int* a, int left, int right)
{
  int mid = GetMidIndex(a, left, right);
  Swap(&a[left], &a[mid]);
  int key = a[left];//存下来
  int hole = left;
  while (left < right)
  {
  //因为选取在左侧,所以要right先走
  while (left < right && a[right] >= key)//右选小
  {
    right--;
  }
  //选到以后
  a[hole] = a[right];
  hole = right;
  while (left < right && a[left] <= key)//左选大
  {
    left++;
  }
  a[hole] = a[left];
  hole = left;
  //选到左大,右小,进行交换
  if (left < right)
    Swap(&a[left], &a[right]);
  }
  a[hole] = key;
  return hole;
}


3.前后指针版


1669253768278.jpg

int PartSort4(int* a, int left, int right)
{
  int mid = GetMidIndex(a, left, right);
  Swap(&a[left], &a[mid]);
  int keyi = left;
  int prev = left;
  int cur = left+1;
  while (cur <= right)
  {
  if (a[cur] < a[keyi] && ++prev != cur)
    Swap(&a[cur], &a[prev]);
  ++cur;
  }
  Swap(&a[keyi], &a[prev]);
  return prev;
}


4.非递归实现快排

有些公司在面试的时候曾经提出这个问题,当然对于没有接触过的,当时可能就懵了。


我们来分析一下递归的时候,栈做了什么:

1669253789064.jpg

递归版本我们借助栈做了什么?不就是对这组数据不断细分然后进行三数取中再排,之前我们压栈放的这些分组,借助我们自己实现的栈去放也是一样的道理。


只不过我们需要先调用栈:


#include"Stack.h"
#pragma once
#include<stdio.h>
#include<stdbool.h>
#include<assert.h>
#include<stdlib.h>
typedef int STDataType;
typedef struct Stack
{
  STDataType* a;
  int top;
  int capacity;
}ST;
void StackInit(ST* ps);
void StackDestory(ST* ps);
void StackPush(ST* ps,STDataType x);
void StackPop(ST* ps);
STDataType StackTop(ST* ps);
bool StackEmpty(ST* ps);
int StackSize(ST* ps);
void StackInit(ST* ps)
{
  assert(ps);
  ps->a = NULL;
  ps->top = 0;
  ps->capacity = 0;
}
void StackDestory(ST* ps)
{
  assert(ps);
  free(ps->a);
  ps->a = NULL;
  ps->top = ps->capacity = 0;
}
void StackCheckCapacity(ST* ps)
{
  assert(ps);
  if (ps->top == ps->capacity)
  {
  int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
  STDataType* newStack = (STDataType*)realloc(ps->a, sizeof(STDataType) * newcapacity);
  if (newStack == NULL)
  {
    perror("realloc fail");
    exit(-1);
  }
  else
  {
    ps->a = newStack;
    ps->capacity = newcapacity;
  }
  }
}
void StackPush(ST* ps, STDataType x)
{
  assert(ps);
  StackCheckCapacity(ps);
  ps->a[ps->top] = x;
  ++ps->top;
}
bool StackEmpty(ST* ps)
{
  assert(ps);
  return ps->top == 0;
}
void StackPop(ST* ps)
{
  assert(ps);
  assert(!StackEmpty(ps));
  --ps->top;
}
STDataType StackTop(ST* ps)
{
  assert(ps);
  return ps->a[ps->top - 1];
}
int StackSize(ST* ps)
{
  assert(ps);
  return ps->top;
}
void QuickSortNonr(int* a, int left, int right)
{
  ST st;
  StackInit(&st);
  StackPush(&st, left);
  StackPush(&st, right);
  while (!StackEmpty(&st))
  {
  int right = StackTop(&st);
  StackPop(&st);
  int left = StackTop(&st);
  StackPop(&st);
  int keyi=PartSort4(a, left, right);
  if (keyi + 1 < right)
  {
    StackPush(&st, keyi+1);
    StackPush(&st, right);
  }
  if (left < keyi - 1)
  {
    StackPush(&st, left);
    StackPush(&st, keyi - 1);
  }
  }
  StackDestory(&st);
}

快速排序特性总结:


1. 快速排序整体的综合性能和使用场景都是比较好的。

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(logN).

4. 稳定性:不稳定.


五、归并排序


基本思想:

归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法( Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

1669253822279.jpg

1669253832680.jpg


5.1递归版归并排序


我们观察归并排序的过程,就会发现,它本质是不断细分,(不断递归),直到只剩两个数,然后借助第三空间,取小尾插,再返回上一级,层层递进,最后再把第三空间排号的数拷贝到原数组中,这时候就完成了归并的过程。


//归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)
  return;
  int mid = begin + (end - begin) / 2;
  _MergeSort(a, begin, mid, tmp);
  _MergeSort(a, mid + 1, end, tmp);
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  int i = begin;
  while (begin1 <= end1 && begin2 <= end2)
  {
  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++];
  }
  memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
  perror("malloc fail");
  return;
  }
  _MergeSort(a, 0, n - 1, tmp);
  free(tmp);
  tmp = NULL;
}


5.2非递归版归并排序


非递归版要比递归版本考虑更多情形,我们知道递归版本是递归到2个数据归并,然后返回,变为4个数据归并,不断返回。如果,我们刚开始就把数组里的数据从2个开始归并,然后开始不断扩大区间。


1669253860062.jpg


因为数据的个数不一定是整数倍,但是我们计算是按照整数倍来计算的,存在越界的情况,因此我们要修正一些场景。


1669253869344.jpg


我们要想明白归并排序是两组数据归并之后进行比较,取小的尾插,因此必须要有两组数据进行比较,我们分析越界情况有如下几种:


1.第一组end1越界。这种情况就没有了第二组,因此直接break,让它在其他gap值重新分组归并。


2.第二组全部越界。这种也是只有第一组


3.第二组越界部分越界。这种情况需要修正边界,修正第二组的right为数组末尾。可以比较。


void MergeSortNonR(int* a,int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  if (tmp == NULL)
  {
  perror("malloc fail");
  return;
  }
  int gap = 1;
  while (gap < n)
  {
  //gap个数据归并
  for (int j = 0; j < n; j +=2* gap)//因为是两组数据
  {
    int begin1 = j, end1 = j + gap - 1;
    int begin2 = j + gap, end2 = j + gap * 2 - 1;
    //第一组部分越界
    if (end1 >= n)
    {
    break;
    }
    //第二组全部越界
    if (begin2 >= n)
    {
    break;
    }
    //第二组部分越界
    if (end2 >= n)
    {
    end2 = n - 1;
    }
    int i = j;
    while (begin1 <= end1 && begin2 <= end2)
    {
    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++];
    }
    memcpy(a + j, tmp + j, (end2 - j + 1) * sizeof(int));
  }
  gap *= 2;
  }
  free(tmp);
  tmp = NULL;
}


归并排序的特性总结:

1. 归并的缺点在于需要 O(N) 的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

2. 时间复杂度: O(N*logN)

3. 空间复杂度: O(N)

4. 稳定性:稳定

六、非比较排序(计数排序)

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:

1. 统计相同元素出现次数。

2. 根据统计的结果将序列回收到原来的序列中。

计数排序的思想很有趣,统计数组中每个数出现的次数存到另一个数组中,另一个数组的下标加上最小的值就是要排序数组里的元素,而这些下标对应的值就是它出现的次数。

void CountSort(int* a, int n)
{
  int max = a[0], min = a[0];
  for (int i = 1; i < n; ++i)
  {
  if (a[i] > max)
    max = a[i];
  if (a[i] < min)
    min = a[i];
  }
  int range = max - min+1;//这是范围
  int* countA = (int*)malloc(sizeof(int) * range);
  if (countA == NULL)
  {
  perror("malloc fail");
  return;
  }
  memset(countA, 0, sizeof(int) * range);
  for (int i = 0; i < n; ++i)
  {
  countA[a[i] - min]++;
  }
  int j = 0;
  for (int i = 0; i < range; ++i)
  {
  while (countA[i]--)
  {
    a[j]=i+min;
    ++j;
  }
  }
  free(countA);
}


计数排序的特性总结:

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。

2. 时间复杂度: O(MAX(N, 范围 ))

3. 空间复杂度: O( 范围 )

4. 稳定性:稳定


七、排序算法复杂度及稳定性分析


 1669253912068.jpg

1669253920754.jpg


相关文章
|
5月前
orderBy排序,null在最后
orderBy排序,null在最后
72 0
|
9月前
对List进行排序,值为null的排到最后
对List进行排序,值为null的排到最后
|
4月前
|
小程序
排序sort()排序用法
排序sort()排序用法
|
6月前
排序(Sort)(一)
排序(Sort)(一)
49 0
|
6月前
排序(Sort)(二)
排序(Sort)(二)
41 0
|
11月前
sort()排序以及多个属性数组对象排序(按条件排序)
sort()排序以及多个属性数组对象排序(按条件排序)
57 0
|
搜索推荐 算法
|
vr&ar
【LeetCode1122】数组的相对排序(哈希or自定义sort)
没错,,简单题,就是说将arr1中有arr2的元素,则按照arr2的元素先排列(特别注意:这里的arr2的元素都是不同的,但是arr1中是可以元素重复的)
148 0
【LeetCode1122】数组的相对排序(哈希or自定义sort)
|
XML SQL 数据库
除了会排序,你对ORDER BY的用法可能一无所知!(二)
小伙伴们在进行SQL排序时,都能很自然的使用到ORDER BY。不管是默认ASC的升序,还是DESC降序,几乎都是信手拈来。 今天给大家分享一些你可能不知道的ORDER BY用法。
除了会排序,你对ORDER BY的用法可能一无所知!(二)
|
SQL 数据库
除了会排序,你对ORDER BY的用法可能一无所知!(一)
小伙伴们在进行SQL排序时,都能很自然的使用到ORDER BY。不管是默认ASC的升序,还是DESC降序,几乎都是信手拈来。 今天给大家分享一些你可能不知道的ORDER BY用法。
除了会排序,你对ORDER BY的用法可能一无所知!(一)