数据结构__<八大排序> __插入排序 |希尔排序 |选择排序 |堆排序 |快速排序 |归并排序(C语言实现)

简介: 数据结构__<八大排序> __插入排序 |希尔排序 |选择排序 |堆排序 |快速排序 |归并排序(C语言实现)

前言目录

插入排序

//直接插入排序
void InsertSort(int* a, int n)
{
  // i的取值范围:[0,n-2]
  for (int i = 0; i < n - 1; i++)
  {
    //每一趟排序
    int end = i;
    int tmp = a[end + 1]; //将tmp视为插入的数字
    while (end >= 0)
    {
      if (tmp < a[end]) //若插入的数字小于有序数字的最后一个数
      {
        a[end + 1] = a[end]; //将大于tmp的值往后挪
        --end;
      }
      else
      {
        break;
      }
    }
    a[end + 1] = tmp;
  }
}

1、元素集合越接近有序,直接插入排序算法的时间效率越高

2、时间复杂度:O(N^2)

3、空间复杂度:O(1),它是一种稳定的排序算法

4、稳定性:稳定

希尔排序

//希尔排序
void ShellSort(int* a, int n)
{
  //1、gap > 1 预排序
  //2、gap == 1 直接插入排序
  int gap = n;
  while (gap > 1)
  {
    gap = gap / 3 + 1; //+1能够保证最后一次gap一定是1
                       //控制gap组都进行预排序
        //这里只是把插入排序的1换成gap即可
    //但是这里不是排序完一个分组,再去
    //排序另一个分组,而是整体只过一遍
    //这样每次对于每组数据只排一部分
    //整个循环结束之后,所有组的数据排序完成
    for (int i = 0; i < n - gap; i++)
    {
      //确保一组中的数据都进行插入排序
      int end = i;
      //定义一个变量tmp保存end的后一个数,其下标是end+gap
      int tmp = a[end + gap];
      while (end >= 0)
      {
        if (a[end] > tmp)
        {
          a[end + gap] = a[end];//如果end下标的数值大于后面的值tmp,也就意味着end下标的值要往后挪
          end -= gap;
        }
        else
        {
          break;
        }
      }
      //单趟循环结束或循环中直接break出来均直接赋值
      a[end + gap] = tmp;
    }
  }
}

1、希尔排序是对直接插入排序的优化。


2、当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。


3、稳定性:不稳定


4、时间复杂度分析:


希尔排序的时间复杂度不是很好算,我们先简要看下预排序的时间复杂度:


gap很大时,数据跳的很快,里面套的循环可以忽略不记,差不多是O(N)。

gap很小时,数据跳的很慢,很接近有序,差不多也是O(N)

再来看外面套上循环后的时间复杂度:

while循环中的gap = gap / 3 + 1相当于是循环了

既然外循环执行次,内循环执行N次,那么时间复杂度为O()。但是上述计算顶多是估算,有人在大量的实验基础上推出其时间复杂度应为:O()

选择排序

思路:

优化的选择排序,每次可以选择一个最大的和一个最小的,然后把他们放在合适的位置,即最小的放在第一个位置,最大的放在最后一个位置,然后再去选择次小的和次大的,依次这样进行,直到区间只剩一个值或没有

#include<iostream>
#include<string>
#include<stdlib.h> 
using namespace std;
//交换
void Swap(int* pa, int* pb)
{
  int tmp = *pa;
  *pa = *pb;
  *pb = tmp;
}
void SelectSort(int* a, int n)
{
  //assert(a);
  int begin = 0, end = n - 1;
  while (begin < end)
  {
    int min = begin, max = begin;
    for (int i = begin; i <= end; i++)
    {
      if (a[i] >= a[max])
        max = i;
      if (a[i] < a[min])
        min = i;
    }
    //最小的放在
    Swap(&a[begin], &a[min]);
    //如果最大的位置在begin位置
    //说明min是和最大的交换位置
    //这个时候max的位置就发生了变换
    //max变到了min的位置
    //所以要更新max的位置
    if (begin == max)
      max = min;
    Swap(&a[end], &a[max]);
    ++begin;
    --end;
    //PrintArray(a, n);
  }
}
void PrintArray(int a[], int sum)
{
  for(int i=0;i<sum;i++)
  {
    cout<<a[i]<<" ";
  }
  cout<<endl;
}
void TestSelectSort()
{
  int a[] = { 3, 4, 6, 1, 2, 8, 0, 5, 7 };
  SelectSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
  TestSelectSort();
  return 0;
}

1、直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

2、时间复杂度:O(N^2)

3、空间复杂度:O(1)

4、稳定性:不稳定


易错题:

使用选择排序对长度为100的数组进行排序,则比较的次数为( )

A.5050

B.4950

C.4851

D.2475


答案:B

解析:

选择排序,每次都要在未排序的所有元素中找到最值,

如果有n个元素,则

第一次比较次数: n - 1

第二次比较次数: n - 2

....

第n - 1次比较次数: 1

所有如果n = 100

则比较次数的总和:99 + 98 + ...... + 1

共4950次。

堆排序

1,建大堆,把根交换到最底,然后在减一个元素继续调整

2,向下调整,继续交换,直到最后一个元素

上图的代码:

void HeapSort(int* a, int n)
{
  //向下调整建堆
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(a, n, i);
  }
  //大堆升序
  size_t end = n - 1;
  while (end > 0)
  {
    Swap(&a[0], &a[end]);    //为了保持完全二叉树状态
    AdjustDown(a, end, 0);
    end--;
  }
}
#include<iostream>
#include<string>
#include<stdlib.h> 
using namespace std;
//交换
void Swap(int* pa, int* pb)
{
  int tmp = *pa;
  *pa = *pb;
  *pb = tmp;
}
//向下调整算法
void AdjustDown(int* a, size_t size, size_t root)
{
  int parent = (int)root;
  int child = 2 * parent + 1;
  while (child < size)
  {
    //1、确保child的下标对应的值最大,即取左右孩子较大那个
    if (child + 1 < size && a[child + 1] > a[child]) //得确保右孩子存在
    {
      child++; //此时右孩子大
    }
    //2、如果孩子大于父亲则交换,并继续往下调整
    if (a[child] > a[parent])
    {
      Swap(&a[child], &a[parent]);
      parent = child;
      child = 2 * parent + 1;
    }
    else
    {
      break;
    }
  }
}
//升序
void HeapSort(int* a, int n)
{
  //向下调整建堆
    // 建堆,先从最后两个叶子上的根(索引为(n - 2) / 2开始建堆
  // 先建最小的堆,直到a[0](最大的堆)
  // 这就相当于在已经建好的堆上面,新加入一个
  // 根元素,然后向下调整,让整个完全二叉树
  // 重新满足堆的性质
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(a, n, i);
  }
  //大堆升序
  size_t end = n - 1;
  while (end > 0)
  {
    Swap(&a[0], &a[end]);
    AdjustDown(a, end, 0);
    end--;
  }
}
int main()
{
  int a[] = { 4,2,7,8,5,1,0,6 };
  HeapSort(a, sizeof(a) / sizeof(int));
  for (int i = 0; i < sizeof(a) / sizeof(int); i++)
  {
    printf("%d ", a[i]);
  }
  return 0;
}

1、堆排序使用堆来选数,效率就高了很多。

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

3、空间复杂度:O(1)

4、稳定性:不稳定

冒泡排序

//交换
void Swap(int* pa, int* pb)
{
  int tmp = *pa;
  *pa = *pb;
  *pb = tmp;
}
//冒泡排序
void BubbleSort(int* a, int n)
{
  for (int j = 0; j < n; j++)
  {
    for (int i = 1; i < n - j; i++)
    {
      if (a[i - 1] > a[i])
      {
        Swap(&a[i], &a[i - 1]);
      }
    }
  }
}

1、冒泡排序是一种非常容易理解的排序

2、稳定性:稳定

3、空间复杂度:O(1)

4、时间复杂度:O(N^2)

最好情况:数组本身是顺序的,外层循环遍历一次就完成

最坏情况:数组本身是逆序的,内外层遍历

快速排序

hoare法

  1. 选出一个key,一般是第一个数,或者是最后一个数
  2. 定义变量L和R,L从左走,R从右走
  3. R先向前走,找到比key小的位置停下,再让L向后走,找到比key大的值停下
  4. 交换L和R代表的数值
  5. 继续遍历,同样让R先走,L后走,同上规则
  6. 当L和R相遇的时候,把相遇位置的值与key位置的值交换,结束
//hoare
//快排单趟排序
int PartSort(int* a, int left, int right)
{
  int keyi = left; //选左边作key
  while (left < right)
  {
    //右边先走,找小
    while (left < right && a[right] >= a[keyi]) //防止right找不到比keyi小的值直接飙出去,要加上left < right
    {
      right--;
    }
    //右边找到后,左边再走,找大
    while (left < right && a[left] <= a[keyi]) //同上,也要加上left < right
    {
      left++;
    }
    //右边找到小,左边找到大,就交换
    Swap(&a[left], &a[right]);
  }
  //此时left和right相遇,交换与key的值
  Swap(&a[keyi], &a[left]);
  return left;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
  //子区间相等只有一个值或者不存在那么就是递归结束的子问题
  if (begin >= end)
  {
    return;
  }
  int keyi = PartSort(a, begin, end);
  //分成左右两段区间递归
  // [begin, keyi-1] 和 [keyi+1, end]
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi + 1, end);
}

挖坑法

把最左边的位置用key保存起来,此位置形成坑位

定义变量L和R分别置于最左和最右

让R先向前走,找到比key小的位置停下

找到后,将该值放入坑位,自己形成新的坑位

再让L向后走,找比key大的位置停下

找到后,将该值放入坑位,自己形成新的坑位

再让R走……

当L和R相遇时,把key的值放到坑位,结束

//挖坑法
int PartSort2(int* a, int left, int right)
{
  //把最左边的值用key保存起来
  int key = a[left]; 
  //把left位置设为坑位pit
  int pit = left;
  while (left < right) //当left小于right时就继续
  {
    //右边先走,找小于key的值
    while (left < right && a[right] >= key)
    {
      right--; //如若right的值>=key的值就继续
    }
    //找到小于key的值时就把此位置赋到坑位,并把自己置为新的坑位
    a[pit] = a[right];
    pit = right;
    //左边走,找大于key的值
    while (left < right && a[left] <= key)
    {
      left++;
    }
    //找到大于key的值就把此位置赋到坑位,并把自己置为新的坑位
    a[pit] = a[left];
    pit = left;
  }
  //此时L和R相遇,将key赋到坑位
  a[pit] = key;
  return pit;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
  //子区间相等只有一个值或者不存在那么就是递归结束的子问题
  if (begin >= end)
  {
    return;
  }
  int keyi = PartSort2(a, begin, end);
  //分成左右两段区间递归
  // [begin, keyi-1] 和 [keyi+1, end]
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi + 1, end);
}

前后指针法

把第一个位置的值设为key保存起来

定义prev指针指向第一个位置,cur指向prev后一个位置

若cur指向的数值小于key,prev和cur均后移

当cur指向的数据大于key时,prev不动,cur继续后移

当cur的值小于key时,prev后移一位,交换与cur的值,cur再++

重复上述操作,当cur越界时,交换此时的prev和key的值。结束

 

//前后指针法
int PartSort3(int* a, int left, int right)
{
  int key = left;//注意不能写成 int key = a[left]
  int prev = left;
  int cur = prev + 1;
  while (cur <= right)
  {
    if (a[cur] < a[key] && a[++prev] != a[cur]) 
    {
      Swap(&a[prev], &a[cur]);//在cur的值小于key的值的前提下,并且prev后一个值不等于cur的值时交换,避免了交换两个小的(虽然也可以,但是没有意义)
    }
    cur++; //如若cur的值大于key,则cur++
  }
  Swap(&a[prev], &a[key]); //此时cur越界,直接交换key与prev位置的值
  return prev;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
  //子区间相等只有一个值或者不存在那么就是递归结束的子问题
  if (begin >= end)
  {
    return;
  }
  int keyi = PartSort2(a, begin, end);
  //分成左右两段区间递归
  // [begin, keyi-1] 和 [keyi+1, end]
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi + 1, end);
}

快排特性总结

稳定性:不稳定


空间复杂度:O(logN)


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


快排的时间复杂度分两种情况讨论:


最好:每次选key都是中位数,通俗讲是左边一半右边一半,具体看是key的左序列长度和右序列长度相同。时间复杂度O(N*logN)

最坏:每次选出最小的或者最大的作为key。时间复杂度O()


易错题:


排序过程中,对尚未确定最终位置的所有元素进行一遍处理称为一“趟”。下列排序中,不可能是快速排序第二趟结果的是()【2019年全国试题10(2分)】


A. 5, 2, 16, 12, 28, 60, 32, 72

B. 2, 16, 5, 28, 12, 60, 32, 72

C. 2, 12, 16, 5, 28, 32, 72, 60

D. 5, 2, 12, 28, 16, 32, 72, 60


答案:D


每经过一趟快排,轴点元素都必然就位。也就是说,一趟下来至少有1个元素在其最终位置。所以考察各个选项,看有几个元素就位即可。


最终排序位置是:2, 5, 12, 16, 28, 32, 60, 72,而选项中正确的位置有:


A. 5, 2, 16, 12, 28, 60, 32, 72

B. 2, 16, 5, 28, 12, 60, 32, 72

C. 2, 12, 16, 5, 28, 32, 72, 60

D. 5, 2, 12, 28, 16, 32, 72, 60



对于D选项,在第一趟排序好,一定能确定一个枢轴元素,要么是12,要么是32,如果是12的话,在第二趟向左递归的时候,一定是2排在5的前面,如果第二趟是先向右递归,那么16肯定排在28的前面,。如果是32的话,跟上面的一个思路,在第二趟排序的时候,如果先向左递归,5一定排在2的后面,如果先向右递归,60也一定排到72的前面,综上,这两种情况d选项都不符合。

三数取中优化

取第一个数,最后一个数,中间那个数,在这三个数中选不是最大也不是最小的那个数作为key。此法针对有序瞬间从最坏变成最好,针对随机数,那么选出来的数也同样不是最大也不是最小,同样进行了优化。

三数取中其实针对hoare法,挖坑法,前后指针法都适用,这里我们就以前后指针法示例:

//快排
//三数曲中优化
int GetMidIndex(int* a, int left, int right)
{
  int mid = (left + right) / 2; // int mid = left + (right - left) / 2
  // left  mid  right
  if (a[left] < a[mid])
  {
    if (a[mid] < a[right]) // left < mid < right
      return mid;
    else if (a[left] < a[right]) // left < right <mid
      return right;
    else  // right < left < mid
      return left; 
  }
  else // left > mid
  {
    if (a[right] > a[left]) // right > left > mid
      return left;
    else if (a[mid] > a[right])// left > mid > right
      return mid;
    else // left > right > mid
      return right;
  }
}
//前后指针法
int PartSort3(int* a, int left, int right)
{
  //三数取中优化
  int midi = GetMidIndex(a, left, right);
  Swap(&a[midi], &a[left]);
  int key = left;//注意不能写成 int key = a[left]
  int prev = left;
  int cur = prev + 1;
  while (cur <= right)
  {
    if (a[cur] < a[key] && a[++prev] != a[cur]) 
    {
      Swap(&a[prev], &a[cur]); 
    }
    cur++;  
  }
  Swap(&a[prev], &a[key]);  
  return prev;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
  //子区间相等只有一个值或者不存在那么就是递归结束的子问题
  if (begin >= end)
  {
    return;
  }
  int keyi = PartSort3(a, begin, end);
  //分成左右两段区间递归
  // [begin, keyi-1] 和 [keyi+1, end]
  QuickSort(a, begin, keyi - 1);
  QuickSort(a, keyi + 1, end);
}

小区间优化

当递归到越小的区间时,递归次数就会越多,针对这一小区间采取插入排序更优,减少了大量的递归次数

//三数取中优化
int GetMidIndex(int* a, int left, int right)
{
    //……
}
//前后指针法
int PartSort3(int* a, int left, int right)
{
  //三数取中优化
  int midi = GetMidIndex(a, left, right);
  Swap(&a[midi], &a[left]);
    //……
}
//小区间优化
void QuickSort2(int* a, int begin, int end)
{
  //子区间相等只有一个值或者不存在那么就是递归结束的子问题
  if (begin >= end)
  {
    return;
  }
  //小区间直接插入排序控制有序
  if (end - begin + 1 <= 10)
  {
    InsertSort(a + begin, end - begin + 1);
  }
  else
  {
    int keyi = PartSort3(a, begin, end);
    // [begin, keyi-1] 和 [keyi+1, end]
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
  }
}

快排非递归

在快排递归的过程中是要建立栈帧的,仔细看看每次递归时传的参数,有begin和end,其递归过程存储的是排序过程中要控制的区间,那我们用非递归模拟递归的过程中也要按照它这个存储方式进行,这就需要借助

//快排非递归
void QuickSort3(int* a, int begin, int end)
{
  ST st;
  StackInit(&st);
  //先把第一块区间入栈
  StackPush(&st, begin);
  StackPush(&st, end);
  while (!StackEmpty(&st)) //栈不为空就继续
  {
    int right = StackTop(&st);
    StackPop(&st);
    int left = StackTop(&st);
    StackPop(&st);
    //使用前后指针法进行排序
    int keyi = PartSort3(a, left, right); // keyi已经到了正确位置
    // [left, kryi-1]  [keyi+1, right]
    if (left < keyi - 1)//如若左区间不只一个数就入栈
    {
      StackPush(&st, left);
      StackPush(&st, keyi - 1);
    }
    if (keyi + 1 < right)//若右区间不只一个就入栈
    {
      StackPush(&st, keyi + 1);
      StackPush(&st, right);
    }
  }
  StackDestory(&st);
}

归并排序

如图所示,先分为最小单元,利用数组tmp排序,然后回溯重复操作

void _MergeSort(int* a, int begin, int end, int* tmp)
{
  if (begin >= end)
    return; //区间不存在就返回
  int mid = (begin + end) / 2;
  //[begin, mid] [mid+1, end]
  _MergeSort(a, begin, mid, tmp); //递归左半
  _MergeSort(a, mid + 1, end, tmp); //递归右半
  //归并[begin, mid] [mid+1, end]
  //printf("归并[%d,%d][%d,%d]\n", begin, mid, mid + 1, end);
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  int index = begin;
  while (begin1 <= end1 && begin2 <= end2)
  {
    //将较小的值放到tmp数组里头
    if (a[begin1] < a[begin2])
    {
      tmp[index++] = a[begin1++];
    }
    else
    {
      tmp[index++] = a[begin2++];
    }
  }
  //如若begin2先走完,把begin1后面的元素拷贝到新数组
  while (begin1 <= end1)
  {
    tmp[index++] = a[begin1++];
  }
  //如若begin1先走完,把begin2后面的元素拷贝到新数组
  while (begin2 <= end2)
  {
    tmp[index++] = a[begin2++];
  }
  //归并结束后,把tmp数组拷贝到原数组
  memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
//归并排序
void MergeSort(int* a, int n)
{
  //malloc一块新数组
  int* tmp = (int*)malloc(sizeof(int) * n);
  assert(tmp);
  _MergeSort(a, 0, n - 1, tmp);
  free(tmp);
}

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

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

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

4、稳定性:稳定

归并排序非递归

  • 思想:

归并的非递归不需要借助栈,直接使用循环即可。递归版中我们是对数组进行划分成最小单位,这里非递归我们直接把它看成最小单位进行归并。我们可以通过控制间距gap来完成

//归并非递归
void MergeSortNonR(int* a, int n)
{
  int* tmp = (int*)malloc(sizeof(int) * n);
  assert(tmp);
  int gap = 1;
  while (gap < n)
  {
    //分组归并,间距为gap是一组,两两归并
    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;
      //end1越界,修正即可
      if (end1 >= n)
      {
        end1 = n - 1;
      }
      //begin2越界,第二个区间不存在
      if (begin2 >= n)
      {
        begin2 = n;
        end2 = n - 1;
      }
      //begin2 ok,end2越界,修正下end2即可
      if (begin2 < n && end2 >= n)
      {
        end2 = n - 1;
      }
      printf("归并[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
      int index = i;
      while (begin1 <= end1 && begin2 <= end2)
      {
        //将较小的值放到tmp数组里头
        if (a[begin1] < a[begin2])
        {
          tmp[index++] = a[begin1++];
        }
        else
        {
          tmp[index++] = a[begin2++];
        }
      }
      //如若begin2先走完,把begin1后面的元素拷贝到新数组
      while (begin1 <= end1)
      {
        tmp[index++] = a[begin1++];
      }
      //如若begin1先走完,把begin2后面的元素拷贝到新数组
      while (begin2 <= end2)
      {
        tmp[index++] = a[begin2++];
      }
    }
    memcpy(a, tmp, n * sizeof(int));
    gap *= 2;
  }
  free(tmp);
}

优化+完整版

/*
非递归排序与递归排序相反,将一个元素与相邻元素构成有序数组,
再与旁边数组构成有序数组,直至整个数组有序。
要有mid指针传入,因为不足一组数据时,重新计算mid划分会有问题
需要指定mid的位置
*/
void merge(int* a, int left, int mid, int right, int* tmp)
{
  // [left, mid]
  // [mid+1, right]
  int begin1 = left, end1 = mid;
  int begin2 = mid + 1, end2 = right;
  int index = left;
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] <= a[begin2])
      tmp[index++] = a[begin1++];
    else
      tmp[index++] = a[begin2++];
  }
  while (begin1 <= end1)
  {
    tmp[index++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[index++] = a[begin2++];
  }
  memcpy(a+left, tmp+left, sizeof(int)*(right - left+1));
}
/*
k用来表示每次k个元素归并
*/
void mergePass(int *arr, int k, int n, int *temp)
{
  int i = 0;
  //从前往后,将2个长度为k的子序列合并为1个
  while(i < n - 2*k + 1)
  {
    merge(arr, i, i + k - 1, i + 2*k - 1, temp);
    i += 2*k;
  }
  //合并区间[i, n - 1]有序的左半部分[i, i + k - 1]以及不及一个步长的右半部分[i + k, n - 1]
  if(i < n - k )
  {
    merge(arr, i, i + k - 1,n-1, temp);
  }
}
// 归并排序非递归版
void MergeSortNonR(int *arr,int length)
{
  int k = 1;
  int *temp = (int *)malloc(sizeof(int) * length);
  while(k < length)
  {
    mergePass(arr, k, length, temp);
    k *= 2;
  }
  free(temp);
}
void TestMergeSort()
{
  int a[] = { 3, 4, 6, 1, 2, 8, 0, 5, 7 };
  MergeSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
}

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

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

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

4、稳定性:稳定

计数排序

绝对映射:原数组是几,映射到新数组下标位置++

相对映射:此时新数组下标的范围是从0到原数组最小的值,而映射到下标的位置为原数组val的值 - 原数组最小min的值

//计数排序
void CountSort(int* a, int n)
{
  int min = a[0], max = a[0];
  //先求出原数组的最大和最小值
  for (int i = 1; i < n; i++)
  {
    if (a[i] < min)
      min = a[i];
    if (a[i] > max)
      max = a[i];
  }
  //求出新数组的范围
  int range = max - min + 1;
  //开辟新数组
  int* countA = (int*)malloc(sizeof(int) * range);
  assert(countA);
  //把新开辟数组初始化为0
  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; //赋值时,记得加回原先的min
    }
  }
  free(countA);
}

完整版

void CountSort(int* a, int n)
{
  int max = a[0], min = a[0];
  for (int i = 0; i < n; ++i)
  {
    if (a[i] > max)
      max = a[i];
    if (a[i] < min)
      min = a[i];
  }
  //找到数据的范围
  int range = max - min + 1;
  int* countArray = (int*)malloc(range*sizeof(int));
  memset(countArray, 0, sizeof(int)*range);
  //存放在相对位置,可以节省空间
  for (int i = 0; i < n; ++i)
  {
    countArray[a[i] - min]++;
  }
  //可能存在重复的数据,有几个存几个
  int index = 0;
  for (int i = 0; i < range; ++i)
  { 
    while (countArray[i]--)
    {
      a[index++] = i+min;
    }
  }
}
void TestCountSort()
{
  int a[] = { 3, 4, 6, 2, 8, 5, 2, 2, 9, 9, 1000000, 99999};
  CountSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
}
void TestSortOP()
{
  const int n = 1000000;
  int* a1 = (int*)malloc(sizeof(int)*n);
  int* a2 = (int*)malloc(sizeof(int)*n);
  int* a3 = (int*)malloc(sizeof(int)*n);
  int* a4 = (int*)malloc(sizeof(int)*n);
  int* a5 = (int*)malloc(sizeof(int)*n);
  int* a6 = (int*)malloc(sizeof(int)*n);
  int* a7 = (int*)malloc(sizeof(int)*n);
  int* a8 = (int*)malloc(sizeof(int)*n);
  srand(time(0));
  for (int i = 0; i < n; ++i)
  {
    a1[i] = rand();
    a2[i] = a1[i];
    a3[i] = a1[i];
    a4[i] = a1[i];
    a5[i] = a1[i];
    a6[i] = a1[i];
    a7[i] = a1[i];
    a8[i] = a1[i];
  }
  a8[n] = 100000000;
  size_t begin1 = clock();
  //InsertSort(a1, n);
  size_t end1 = clock();
  printf("%u\n", end1 - begin1);
  size_t begin2 = clock();
  ShellSort(a2, n);
  size_t end2 = clock();
  printf("%u\n", end2 - begin2);
  size_t begin3 = clock();
  //SelectSort(a3, n);
  size_t end3 = clock();
  printf("%u\n", end3 - begin3);
  size_t begin4 = clock();
  HeapSort(a4, n);
  size_t end4 = clock();
  printf("%u\n", end4 - begin4);
  size_t begin5 = clock();
  //BubbleSort(a5, n);
  size_t end5 = clock();
  printf("%u\n", end5 - begin5);
  size_t begin6 = clock();
  QuickSort(a6, 0, n-1);
  size_t end6 = clock();
  printf("%u\n", end6 - begin6);
  size_t begin7 = clock();
  MergeSort(a7, n);
  size_t end7 = clock();
  printf("%u\n", end7 - begin7);
  size_t begin8 = clock();
  CountSort(a8, n);
  size_t end8 = clock();
  printf("%u\n", end8 - begin8);
}
  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度:O(N + range)
  3. 空间复杂度:O(range)
  4. 稳定性:稳定

总结

  • 内排序:数据量较少,在内存中进行排序
  • 外排序:数据量很大,在磁盘上进行排序
  • 综上1G = 1024*1024*1024Byte,而10亿个整数40亿Byte,所以10亿个整数占4G,即1e9以下可内排序,以上必须外排序

OJ测试

912. 排序数组 - 力扣(LeetCode)

 

/*
此题对于时间效率要求较高,像插入排序,选择排序,冒泡排序都是O(n^2)的时间复杂度,所以这三种排序都跑不过。
*/
int* sortArray(int* nums, int numsSize, int* returnSize){
    //插入排序, 此题如果用插入排序,时间复杂度过高,会导致TLE
    InsertSort(nums, numsSize);
    //希尔
    ShellSort(nums, numsSize);
    //选择,会超出时间限制
    SelectSort(nums, numsSize);
    //冒泡排序, 也会超出时间限制
    BubbleSort(nums, numsSize);
    //快排
    QuickSort(nums, 0, numsSize - 1);
    //归并
    MergeSort(nums, numsSize);
    //计数
    CountSort(nums, numsSize);
    *returnSize = numsSize;
    return nums;
}
相关文章
|
14天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
7天前
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
28 4
|
13天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
56 16
|
13天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
62 8
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
33 3
|
6天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
21 6
|
26天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
34 10
|
19天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
25天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
53 7
|
25天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
29 4