数据结构进阶 八大排序算法详解

数据结构就是定义出某种结构:像数组结构、链表结构、树形结构等,实现数据结构就是我们主动去管理增删查改的实现函数

排序的概念

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作,排序算法在我们做题和实际运用当中非常的广泛

下面我们在vs环境下编程实现八大排序,这里我们分文件编写,在头文件 Sort.h 进行声明,在 Sort.c 当中进行具体的函数定义,在 test.c 当中的做具体的测试实现

先来了解一下头文件 Sort.h 当中接口

#pragma once//防止重复
#include<stdio.h>//输入 输出
#include<stdlib.h>
#include<time.h>//测试性能
#include<string.h>
#include<assert.h>//断言头文件

void PrintArray(int* a, int n);//打印函数

void InsertSort(int* a ,int n);//直接插入排序

void ShellSort(int* a, int n);// 希尔排序

void SelectSort(int* a, int n);// 直接选择排序

void BubbleSort(int* a, int n);// 冒泡排序

void AdjustDown(int* a, int n, int root);//堆的向下调整
void HeapSort(int* a, int n);//堆排序

void QuickSort(int* a, int begin,int end);// 快速排序

void MergeSort(int* a, int n);//归并排序

void CountSort(int* a, int n);// 计数排序

我们知道,函数的定义方法是非常重要的,也是我们需要深入理解的,下面我们详细学习在 Sort.c 当中具体的函数实现

直接插入排序函数

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

//插入排序
//有序的数组中插入数据  保持数组仍然有序
void InsertSort(int* a, int n)//插入排序
{
  //时间复杂度 O(N^2) --逆序
  //最好O(N) 顺序有序 
  for (int i = 0; i < n - 1; i++)
  {
   int end=i;//end给i
   //先把单趟写出来  [0,end] end+1
   int tmp = a[end + 1];
   while (end >= 0)
   {
     if (tmp < a[end])//升序
     //if (tmp > a[end])//降序
       {
	     a[end + 1] = a[end];//依次往后挪动
	     --end;
	   }
       else
	   {
	     break;
	   }
	}
	a[end + 1] = tmp;//0位置
  }
}

希尔排序函数定义

先选定一个整数,把待排序文件中所有记录分成个组,所有距离为gap的分在同一组内,并对每一组内的记录进行排序,并在没有结束前重复上述工作

// 希尔排序 优化插入排序 
//gap越小 越接近有序 gap越大 大的数更快到后面
void ShellSort(int* a, int n)// 希尔排序
{
  // 1.gap>1 预排序  时间复杂度 O(N)*logN
  // 2.gap==1 直接插入排序

  int gap = n;//gap不能给固定的值
  while (gap > 1)//logN
  {
    gap=gap/3+1 ;//可以往三倍下降  保证最后一次是1
    for (int i = 0; i < 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;
	}
  }
}

交换函数定义

这里我们写一个交换函数,下面很多排序算法都用得上

void swap(int* pa, int* pb)//交换函数
{
  int tmp = *pa;
  *pa = *pb;
  *pb = tmp;
}

直接选择排序函数

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

//时间复杂度O(N^2)
void SelectSort(int* a, int n)// 直接选择排序
{
  //依次遍历选出最小(最大)和最左边(最右边)交换 
	
  int left = 0, right = n - 1;
  while (left < right)
  {
    int mini = left, maxi = left;//初始值
    for (int i = left+1; i <= right; ++i)
    {
      if (a[i] < a[mini])
      {
	    mini = i;
      }
	  if (a[i] > a[maxi])
	  {
	     maxi = i;
	   }
	}	
	swap(&a[left], &a[mini]);
    //防止重叠
	if (left == maxi)
	{
	  maxi = mini;
	}
    swap(&a[right], &a[maxi]);
    left++;
    right--;
  }
}

堆排序函数定义

堆排序是指利用堆这种数据结构所设计的一种排序算法,需要注意的是排升序要建大堆,排降序建小堆

//向下调整算法 前提 左右都是小堆或大堆
void Adjustdown(int* a, size_t size, size_t root)
{
  //算孩子
  size_t parent = root;
  size_t child = parent * 2 + 1;//先找左孩子

  while (child < size)//左孩子小于size 说明存在
  {
    //找出左右孩子中小的那个 
    //if (child+1<size&&a[child + 1] < a[child])//右孩子存在且右小于左 小堆
    if (child + 1 < size && a[child + 1] > a[child])//右孩子存在且右大于左 大堆
    {
	 ++child;
    }

    //跟父亲比较 小的换就交换 
    //if (a[child] < a[parent])//小堆
    if (a[child] > a[parent])//换这里换成大堆调整
    {
	 swap(&a[child], &a[parent]);
	 //再从交换的位置继续往下调整 
	 parent = child;
	 child = parent * 2 + 1;
    }
    //在中间就调整好的情况
    else
    {
	 break;
    }
  }
}

//堆排序 完全二叉树
void Heapsort(int* a, int n)//升序 建大堆 /空间复杂度O(1)
{
  //在数组上建堆 
  //向下调整建堆O(N)
  //从倒数第一个非叶子节点 ,最后一个节点(n-1)的父亲 (n-1-1)/2开始
  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);
    //最后一个不看做堆里的 (堆的大小-1)在向下调整
    --end;//大的数依次往后放
  }
}

冒泡排序函数定义

将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

// 冒泡排序 交换
void BubbleSort(int* a, int n)// 冒泡排序 交换
{
  //时间复杂度:O(n^2)
  //最好情况:顺序有序 O(N)
  for (int j = 0; j < n; j++)
  {
    int exchange = 0;
    //单趟
    for (int i = 1; i < n - j; ++i)
    {
	  if (a[i - 1] > a[i])
	  {
	    exchange = 1;//发生交换改成1
	    swap(&a[i - 1], &a[i]);//调用交换函数
	  }
    }

    if (exchange == 0)//没有交换不用再冒泡
    {
	 break;
    }
  }
}

归并排序函数定义

分治思想,先使每个子序列有序,再使子序列段间有序,左后合并得到完全有序的序列

//归并子函数
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] 
  int begin1 = begin, end1 = mid;
  int begin2 = mid + 1, end2 = end;
  int index = begin;//取小的往下面放
  while (begin1 <= end1 && begin2 <= end2)
  {
    if (a[begin1] < a[begin2]) //
    {
     tmp[index++] = a[begin1++];//小的放到temp
    }
    else
    {
     tmp[index++] = a[begin2++];
    }
  }
  //剩下数放后面 下面两个while进一个
  while (begin1 <= end1)
  {
    tmp[index++] = a[begin1++];
  }
  while (begin2 <= end2)
  {
    tmp[index++] = a[begin2++];
  }
  //再将数据拷贝到原数组
  memcpy(a+begin, tmp+begin, (end - begin+1) * sizeof(int));
}

//归并排序
//时间复杂度 O(N*logN) 空间复杂度O(N)
void MergeSort(int* a, int n)递归 绝对的二分
{
  //申请临时新数组放归并数据
  int* tmp = (int*)malloc(sizeof(int) * n);//...
  //递归n个有序数组 取小的尾插到一个新数组归并
  _MergeSort(a, 0, n - 1, tmp);

  free(tmp);//最后free掉开辟的 原数组已经有序了
}

快速排序函数定义(重点)

快速排序整体的综合性能和使用场景在排序算法当中都是比较好的

三数取中法

//三数取中法可以避免快速排序在原数组有序的情况下排序慢的缺点,更好的提高效率
int GetMidIndex(int* a, int left, int right)//三数取中法
{
  int mid = (left + (right - left) / 2);
  //left  mid  right 比较
  if (a[left] < a[mid])
  {
    if (a[mid] < a[right]){return mid;}
    else if(a[left]>a[right]){return left;}
    else{return right;}
  }
  else //(a[left] > a[mid])
  {
    if (a[mid] > a[right]){ return mid;}
    else if (a[left] < a[right]){return left;}
    else{return right;}
  }
}
//快速单趟排序方法一
int PartSort1(int* a,int left,int right)//单趟hoare版本 
{
  //在最右或最左选出一个数字做key 
  //两个指针 R找比key小 L找比key大 找到后交换
  int key = left;
  while (left < right)//左边做key 右边先走 
  {
    //右边找比key小的
    while (left < right && a[right] >= a[key])
    {
	 --right;//大于就继续走
    }
    //左边找比key找大的
    while (left < right && a[left] <= a[key])
    {
	 ++left;//小于继续找
    }
    swap(&a[left], &a[right]);//找到后左右交换
  }
  swap(&a[key], &a[left]);//最后相遇的数在和key交换
  //单趟完成后 在递归让左边和右边继续找key交换

  return left;
}

//快速单趟排序方法二
int PartSort2(int* a, int left, int right)//单趟挖坑法版本
{
  //找key先保存起来
  //单趟
  int key = a[left];//
  int pit = left;//坑在left的位置
  while (left < right)
  {
    //右边先走 找比key小的
    while (left < right && a[right] >= key)
    {
	  --right;
    }
    a[pit] = a[right];//找到后放到坑里
    pit = right; //在形成新的坑
    //左边走 找比key大的
    while (left < right && a[left] <= key)
    {
	  ++left;
    }
    a[pit] = a[left];
    pit = left;//形成新的坑
  }

  a[pit] = key; //最后相遇的位置坑里放我们保存的数据
  return pit;//...返回坑位
  //左边是坑 右边先走 找小往坑里放 形成新的坑 左边再找 放新坑
}

//快速单趟排序方法三
int PartSort3(int* a, int left, int right)//单趟前后指针法版本
{

  int midi = GetMidIndex(a, left, right);//调用三数取中函数
  swap(&a[midi], &a[left]);
  int key = left;//下标
  //两个指针 prev  cur
  //cur依次找到比key小的值后 ++prev然后交换两个值
  int prev = left, cur = left + 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;

}

// 快速排序 递归
//时间复杂度 每次选key是中位数 O(N*logN)
//最坏情况 每次选的key是最小的或最大的 O(N*N)
void QuickSort(int* a, int begin,int end) 
{
  if (begin >= end)//错位就没有数据了
     return;
  if (end - begin+1 < 10)//小区间优化 
  {
    InsertSort(a+begin,end-begin+1);
  }
  else
  {
    //int keyi = PartSort1(a, begin, end);//hoare单趟后排完的
    //int keyi = PartSort2(a, begin, end);//挖坑单趟后排完的
    int keyi = PartSort3(a, begin, end);//前后指针单趟后排完的

    QuickSort(a, begin, keyi - 1);//递归找key排左边...
    QuickSort(a, keyi + 1, end);//递归排找key右边...
  }
}

计数排序函数定义

计数排序是对哈希直接定址法的变形应用, 先统计相同元素出现次数后,在根据统计的结果将序列回收到原来的序列中

//时间复杂度 O(range+N) 空间复杂度 O(range)
//是适用于范围集中的数据 负数也可以 
void CountSort(int* a, int n)//计数排序
{
  //将数组当中最小值放到0位置
  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;//计数数组映射的范围
  int* countArry = (int*)malloc(sizeof(int) * range);//计数新数组
  assert(countArry);
  memset(countArry, 0, sizeof(int) * range);//初始化为0

  //计数
  //遍历原数组 相对位置映射减去0位置最小数
  for (int i = 0; i < n; ++i)
  {
    countArry[a[i] - min]++;//
  }

  //排序 遍历计数的数组 往原数组去写
  int j = 0;
  for (int i = 0; i < range; ++i)//遍历计数的数组
  {
    while (countArry[i]--)//一个值--就是走n次 
    {
	  a[j++] = i + min;//在将映射的加回a数组去
    }
  }
}

打印函数定义

//打印函数方便我们观察数据
void PrintArray(int* a, int n)//打印函数
{
  for (int i = 0; i < n; ++i)
  {
    printf("%d ", a[i]);
  }
  printf("\n");
}

我们用上面接口实现在 test.c 当中的每一个排序的测试案例

#include"sort.h"

void TestInsertSort()//插入排序
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  InsertSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
  //打印 1 2 3 4 5 5 6 7 8 9
}

void TestBubbleSort()//冒泡排序
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  BubbleSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
  //打印 1 2 3 4 5 5 6 7 8 9
}

void TestShellSort()//希尔排序
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  ShellSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
  //打印 1 2 3 4 5 5 6 7 8 9
}

void TestHeapSort()//堆排序
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  HeapSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
  //打印 1 2 3 4 5 5 6 7 8 9
}

void TestSelectSort()//选择排序
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  SelectSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
  //打印 1 2 3 4 5 5 6 7 8 9
}

void TestQuickSort()//快速排序
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  QuickSort(a, 0,sizeof(a) / sizeof(int)-1);
  PrintArray(a, sizeof(a) / sizeof(int));
  //打印 1 2 3 4 5 5 6 7 8 9
}

void TestMergeSort()//归并排序
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  MergeSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
  //打印 1 2 3 4 5 5 6 7 8 9
}

void TestCountSort()//计数排序
{
  int a[] = { 9,1,2,5,7,4,8,6,3,5 };
  CountSort(a, sizeof(a) / sizeof(int));
  PrintArray(a, sizeof(a) / sizeof(int));
  //打印 1 2 3 4 5 5 6 7 8 9
}

int main()
{
  TestInsertSort();//插入排序
  TestBubbleSort();//冒泡排序
  TestShellSort();//希尔排序
  TestSelectSort();//选择排序
  TestQuickSort();//快速排序
  TestMergeSort();//归并排序
  TestHeapSort();//堆排序
  TestCountSort();//计数排序
  return 0;
}

接下来我们在随机生成10万的数据测试一下所有排序的性能对比

// 测试各种排序的性能对比
void TestOP()
{
  srand(time(0));//生成随机数
  const int N = 100000;
  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);
  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];
  }
	
  int begin1 = clock();//毫秒数
  InsertSort(a1, N);//插入排序
  int end1 = clock();

  int begin2 = clock();
  ShellSort(a2, N);//希尔排序
  int end2 = clock();

  int begin3 = clock();
  SelectSort(a3, N);//选择排序
  int end3 = clock();

  int begin4 = clock();
  HeapSort(a4, N);//堆排序
  int end4 = clock();

  int begin5 = clock();
  QuickSort(a5, 0, N - 1);//快速排序
  int end5 = clock();

  int begin6 = clock();
  MergeSort(a6, N);//归并排序
  int end6 = clock();

  int begin7 = clock();
  BubbleSort(a7, N);//冒泡排序 
  int end7 = clock();

  int begin8 = clock();
  CountSort(a8, N);//计数排序
  int end8 = clock();

  printf("InsertSort:%d\n", end1 - begin1);//统计计算时间毫秒数相减
  printf("ShellSort:%d\n", end2 - begin2);
  printf("SelectSort:%d\n", end3 - begin3);
  printf("HeapSort:%d\n", end4 - begin4);
  printf("QuickSort:%d\n", end5 - begin5);
  printf("MergeSort:%d\n", end6 - begin6);
  printf("BubbleSort:%d\n", end7 - begin7);
  printf("CountSort:%d\n", end8 - begin8);
  free(a1);
  free(a2);
  free(a3);
  free(a4);
  free(a5);
  free(a6);
  free(a7);
  free(a8);
}

测试结果如下:

我们可以发现在随机生成的10万个数据中,快速、堆、希尔和归并的效率是比较高的,当然在实际的使用当中我们还要根据具体情况选用合适的排序算法

在Java和C++的学习当中,前期学习数据结构当中的顺序表、链表、二叉树等便于我们后面更好的学习容器,后面会继续分享红黑树和算法的实现

希望这篇文章大家有所收获,我们下篇见

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值