数据结构的排序算法

排序

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
常见的排序算法有以下七种,接下来我会一一介绍,并给出相应的算法实现(本文均以升序为例)
在这里插入图片描述

一、插入排序

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

直接插入排序

基本思想
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移,直到所有的元素都插入,此时就得到一个有序的序列。

假设有数组nums={5,3,6,2,9},现在需要通过直接插入排序对该数组进行排序,图解如下:
在这里插入图片描述
算法实现

vector<int> InsertSort(vector<int> nums)
{
	int i, j;
	for (i = 1; i < nums.size(); i++)
	{
		int tmp = nums[i];//保存当前元素,然后和前一个元素进行比较,如果tmp比前一个元素小,则交换位置,继续和前面的元素比较,小的元素向前移
		for (j = i; j > 0 && nums[j-1] > tmp; j--)
		{
			nums[j] = nums[j - 1];
		}
		nums[j] = tmp;
	}
	return nums;
}

结论:

  • 数组规模较小时,直接插入是最优选择
  • 当数组越接近有序时,排序速度越快
  • 直接选择排序的平均时间复杂度为O(n^2),最好情况:O(n),最坏情况:O(n2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

希尔排序

希尔排序也叫缩小增量排序,顾名思义,就是把数组分为n/2组再进行直接插入排序,在保证数组尽可能有序的情况下进行插排,这也是对直接插入排序的优化。
基本思想
取一个小于n的整数gap作为第一增量,对未排好序的序列进行分组,在一般情况下是分为gap组(当分为n/3组时速度时最快的),所有距离为gap的倍数的记录放在同一个组中,对于组内元素进行直接插入排序;取第二个增量为gap/2,利用上述方法,进行排序,直到gap=1,则排序完成,新的序列即为有序序列。

假设有数组nums={5,3,6,2,9,6},现在需要通过希尔排序对该数组进行排序,图解如下:
在这里插入图片描述

算法实现

vector<int> ShellSort(vector<int> nums)
{
	int j;
	for (int gap = nums.size() / 2; gap; gap /= 2)//先进行分组
	{
		for (int k = 0; k < gap; k++)
		{
			for (int i = gap + k; i < nums.size(); i += gap)
			{
				int tmp = nums[i];
				for (j = i; j > 0 && nums[j - gap] > tmp; j -= gap)//直接插入排序
				{
					nums[j] = nums[j - gap];
				}
				nums[j] = tmp;
			}
		}
	}
	return nums;
}

结论:

  • 数组规模较小时,直接插入是最优选择
  • 当数组越接近有序时,排序速度越快
  • 直接选择排序的平均时间复杂度为O(nlogn)~~O(n^2),最好情况:O(n1.3),最坏情况:O(n2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

二、选择排序

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

简单选择排序

基本思想
在无序序列中找到最小的元素,与该序列的首元素交换位置,然后在剩下的元素中找最小的元素与当前序列(arr[i+1]–arr[n-1])的首元素交换位置,重复上述步骤,直到该序列中只剩下一个元素,即排序完成。

假设有数组nums={5,3,6,2,9},现在需要通过选择排序对该数组进行排序,图解如下
在这里插入图片描述
算法实现

vector<int> SelectSort(vector<int> nums)
{
	for (int i = 0; i < nums.size()-1; i++)
	{
		int min_index = i;
		for (int j = i + 1; j < nums.size(); j++)
		{
			if (nums[j] < nums[min_index])//找出数组中最小的元素
			{
				swap(nums[j], nums[min_index]);
			}
		}
	}
	return nums;
}

结论:

  • 简单选择排序的效率太低,在实际中很少用到。
  • 简单选择排序的平均时间复杂度为O(n^2),最好情况:O(n2),最坏情况:O(n2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

堆排序

基本思想
先将一个无序的二叉树调整为一个大根堆,将最后一个非叶子结点与堆顶元素交换位置,此时,从交换后的这个元素开始执行向下调整算法,重复上述步骤,即可排序完成。
向下调整算法(左右子树必须是一个堆):从根结点开始 ,与他的左右孩子进行比较,与较大的孩子交换位置,重复该过程,直到把该二叉树调整成一个大根堆

假设有数组nums={5,3,6,2,9},现在需要通过堆排序对该数组进行排序,图解如下
在这里插入图片描述
算法实现

//向下调整算法
static void AdjustDown(int *src, int size, int m)//static修饰函数是限制函数的作用域,只能在本文件使用
{
        int cur = m;
        int n;//下标
        while (cur * 2 + 1 < size)//没有叶子结点就跳出
        {
               if (cur * 2 + 2 >= size)//没有右孩子的情况,二叉树不存在没有左孩子,但有右孩子的情况
               {
                       n = cur * 2 + 1;//没有右孩子,即和左孩子比较
               }
               else
               {
                       //此处为大堆
                       if (src[cur * 2 + 1] > src[cur * 2 + 2])//左右孩子都存在的情况
                       {
                              n = cur * 2 + 1;//如果左孩子大于右孩子,即和左孩子比较,否则和右孩子比较
                       }
                       else
                       {
                              n = cur * 2 + 2;
                       }
               }
               if (src[cur] < src[n])//该位置比确定n为下标的孩子小,则需要交换,否则直接跳出
               {
                       int tmp = src[cur];
                       src[cur] = src[n];
                       src[n] = tmp;
                       cur = n;
               }
               else
               {
                       break;
               }
        }
}

int SwapHeap(int *src, int size)
{
        if (size == 0)//判断该堆中是否有元素,即判空
        {
               return;
        }
        size--;
        //写成交换的方式,是为了堆排序
        int tmp = src[0];
        src[0] = src[size];//如果不是为了堆排方便,只需要这一条语句即可
        src[size] = tmp;
        AdjustDown(src, size, 0);
}

//将一个数组调整成堆,即建堆的过程
void HeapSort(int* src, int n)
{
        int i;
        for (i = n / 2 -1; i >= 0; i--)//从最后一个非叶子结点开始向前遍历执行向下调整算法
        {
               AdjustDown(src, n, i);
        }
        for (; n > 1; n--)
        {
               SwapHeap(src, n);
        }
}

结论:

  • 堆排序的平均时间复杂度是O(nlogn),最好情况:O(nlogn),最坏情况:O(nlogn)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

三、交换排序

概念:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。
特点:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

冒泡排序

基本思想
该数组中共有n个元素,从第一个元素开始,两两进行比较,较大的元素向后移,直到最大的元素移到最后,完成一趟排序,重复此过程,直到元素完全排好顺序,总共需要进行n-1趟。

假设有数组nums={5,3,6,2,9},现在需要通过冒泡排序对该数组进行排序,图解如下
在这里插入图片描述
算法实现

vector<int> BubbleSort(vector<int> nums)
{
	for (int i = 0; i < nums.size(); i++)//需要n-1趟比较
	{
		for (int j = 0; j < nums.size() - i - 1; j++)//每趟需要n-i次比较
		{
			if (nums[j] > nums[j + 1])
			{
				swap(nums[j], nums[j + 1]);
			}
		}
	}
	return nums;
}

结论

  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

快速排序

对于较多数据时,快速排序的效率是最高的,常见的快速排序的方法有:
a、双指针法
b、挖坑法
c、Hoare法
d、非递归法

1、双指针法1

基本思想:以数组的第一个元素为基准,第一个指针指向数组头,另一个数组指向数组尾,尾指针找比基准元素小的元素且交换,头指针找比基准元素大的元素且交换,经过一趟排序后,基准元素左边的元素均比基准元素小,右边的元素均比基准元素大;两边元素重复上述步骤,直到数组排好顺序。

算法实现

//交换函数
void swapArgs(int * pa, int * pb)
{
        int tmp;
        tmp = *pa;
        *pa = *pb;
        *pb = tmp;
}
//双指针法1
int doublePointerWay1(int *src, int start, int end)
{
        int a = start;//需要定义两个指针,分别指向数组头和数组尾
        int b = end;
        int flag = 0;//如果flag=1执行a++,否则执行b--
        while (src[b] > src[a])//找到第一次要交换的点
        {
               b--;
        }
        while (a<b)
        {
               swapArgs(src + b, src + a);//先交换再判断
               flag = !flag;
               while (src[b] >= src[a])
               {
                       flag ? a++ : b--;//根据flag判断,要进行a++还是b--
               }
        }
        return flag ? b : a;//如果flag=1,执行b;如果flag=0,执行a
}
 
//快速排序类似二叉树的前序遍历
void dealQuickSort(int *src, int start, int end)
{
        int mid;
        if (start < end)
        {
               mid = doublePointerWay1(src, start, end);//基准元素的位置
               dealQuickSort(src, start, mid - 1);//基准元素左边部分
               dealQuickSort(src, mid + 1, end);//基准元素右边部分
        }
}
//快速排序
void QuickSort(int *src, int n)
{
        dealQuickSort(src, 0, n - 1);
}
2、双指针法2

基本思想:从分割策略
在这里插入图片描述
第一步:选取枢纽元 pivot= arr[(left+right)/2]=4
第二步:枢纽元与right交换;
第三步:定义变量i=left,j=right-1;
第四步:i向右走直到遇到比枢纽元大的数;j向左走直到遇到比枢纽元小的数;若此时i<=j,则交换i,j元素,直到i,j交错;
第五步:将枢纽元与i位置元素交换。

算法实现

int doublePointerWay2(int *src, int start, int end)
{
        int a = start;
        int b = end - 1;
        int mid = (start + end) / 2;//此时src[mid]为枢纽元
        swapArgs(src + mid, src + end);//将枢纽元与最后一个元素交换,将枢纽元保护起来
        while (a<=b)
        {
               while(a < end && src[a]<= src[end])//此时src[end]为枢纽元,a指针向右走,找到比枢纽按大的元素停下来
               {
                       a++;
               }
               while (b >0 && src[b] >= src[end])//b向左走,找到比枢纽元小的元素停下来
               {
                       b--;
               }
               if (a == b && (a == 0 || b == end))//当头和尾相等时,直接跳出,避免死循环
               {
                       break;
               }
               if (a < b)//a,b当前所指的元素进行比较,若a<b则进行交换
               {
                       swapArgs(src + a, src + b);
               }
        }
        swapArgs(src + a, src + end);//最后把枢纽元换回去
        return a;
}
3、挖坑法

双指针法1 的优化
基本思想
以第一个元素为基准值,直接保存在一个tmp中,同时需要一个头指针和一个尾指针,头指针指向数组的第一个元素,尾指针指向数组的最后一个元素,尾指针向右走,找比基准值小的元素,直接用该元素将头指针指向的元素覆盖;头指针向左走,找比基准值大的元素,该元素直接将尾指针指向的元素覆盖,直到头指针和尾指针交错,用基准值覆盖指针指向未被覆盖的那个元素。

算法实现

int digWay(int * src, int start, int end)
{
        int a = start;
        int b = end;
        int flag = 0;//flag=0,执行b--,否则执行a++
        int tmp = src[start];//保存基准值
        while (1)
        {
               while (b>start && src[b] >= tmp)//尾指针向左走,找比基准值小的元素
               {
                       b--;
               }
               if (a < b)//当头指针与尾指针没有交错时,尾指针指向的元素直接覆盖头指针指向的元素
               {
                       src[a] = src[b];
               }
               else//当指针与尾指针交错时(即尾指针向左走没有找到比基准值小的元素说明基准值该数组中最小的元素),直接将基准值覆盖到头指针指向的元素
               {
                       src[a] = tmp;
                       return a;
               }
               while (a<end && src[a] <= tmp)//头指针向右走,找比基准值大的元素
               {
                       a++;
               }
               if (a < b)
               {
                       src[b] = src[a];
               }
               else
               {
                       src[b] = tmp;
                       return b;
               }
        }
}
4、Hoare法

基本思想
取该数组第一个元素,最后一个元素,中间元素,这三个数采用冒泡法(其他方法均可)排序,排好序后中间的这个元素为基准值,头指针从第二个元素开始,尾指针从倒数第二个元素开始(头指针一定比基准值小,尾指针一定比基准值大),然后将基准值与尾指针指向的元素交换位置(保护基准值),尾指针从倒数第三个元素开始,向左走,找比基准值小的元素停下来,头指针向右走,找比基准值大的元素停下来,头指针和尾指针指向的元素交换位置,直到两个指针交错,最后一个指针指向未交换的元素与基准值交换。
算法实现

int HoareWay(int *src, int start, int end)
{
        int a = start + 1;
        int b = end - 2;//最后一个元素为冒泡中最大的那个数,倒数第二个为基准值
        int mid = (start + end) / 2;
        if (src[start] > src[mid])//三个数排序
        {
               swapArgs(src + start, src + mid);
        }
        if (src[mid] > src[end])
        {
               swapArgs(src + mid, src + end);
        }
        if (src[start] > src[mid])
        {
               swapArgs(src + start, src + mid);
        }
        if (end - start <= 2)//该数组的元素最多只有三个,则已经排好序,直接跳出
        {
               return mid;
        }
        
        swapArgs(src + mid,src + end - 1);
        while (a <= b)
        {
               while (a < end-1 && src[a] <= src[end-1])//此时src[end-1]为基准值,a指针向右走,找到比基准值大的元素停下来
               {
                       a++;
               }
               while (b >1 && src[b] >= src[end-1])//b向左走,找到比基准值小的元素停下来
               {
                       b--;
               }
               if (a == b && (a == 1 || b == end-1))//当头和尾相等时,直接跳出,避免死循环
               {
                       break;
               }
               if (a < b)//a,b当前所指的元素进行比较,若a<b则进行交换
               {
                       swapArgs(src + a, src + b);
               }
        }
        swapArgs(src + a, src + end -1);//最后把基准值换回去
        return a;
}
5、非递归法

基本思想
利用队列,采用层序遍历的方法
算法实现

void QuickSortNonR(int *src, int n)
{
        int start = 0, end = n - 1;
        int mid;
        Queue qu;
        QueueInit(&qu);
        QueuePush(&qu, 0);//start和end 入队,即0和n-1入队
        QueuePush(&qu, n-1);
        while (!QueueIsEmpty(&qu))//层序遍历
        {
               start = QueueTop(&qu);
               QueuePop(&qu);
               end = QueueTop(&qu);
               QueuePop(&qu);
               mid = HoareWay(src, start, end);
               if (start < mid)//左孩子存在就入队
               {
                       QueuePush(&qu, start);
                       QueuePush(&qu, mid);
               }
               if (mid+1 < end)//右孩子存在就入队
               {
                       QueuePush(&qu, mid+1);
                       QueuePush(&qu, end);
               }
        }
        QueueDestory(&qu);
}

四、归并排序

分治思想:两个过程,过程1:分;过程2:合
基本思想
“分”:先将一个数组从中间分为两个部分,同理,分开的字数组继续分为为两部分,直到分为单独的元素
“合”:借助一个临时空间,需要三个指针,均指向每个数组的第一个元素,第一个指针和第二个指针指向的元素进行比较,较小的元素存在临时空间中,且指向它的指针向后移一位,重复此过程,直到有一个数组为空,将另一个数组中的元素依次存在临时空间中,这样归并排序就完成了。

假设有数组nums={3,6,2,7,9,4},现在需要通过归并排序对该数组进行排序,图解如下
在这里插入图片描述

算法实现

//实现归并排序的具体算法,利用分治思想,分为“分”与“合”两个部分
void dealMergeSort(int * src, int * tmp, int start, int end)
{
        if (start >= end)
        {
               return;
        }
        int mid = (start + end) / 2;
        dealMergeSort(src, tmp, start, mid);//分治思想,先分,该过程为“分”的前一部分
        dealMergeSort(src, tmp, mid + 1, end);//“分”的后部分
        //分治思想中的“合”
        int a = start;//指向前半部分元素的指针
        int b = mid + 1;//指向后半部分元素的指针
        int c = start;//指向临时数组的指针
        //三个指针分别指向三个数组的起始位置,a和b两个指针指向的元素进行比较,较小的元素存在临时数组中
        while (a <= mid && b <= end)
        {
               if (src[a] < src[b])//谁小把谁存在临时数组中,且指向它的指针加1
               {
                       tmp[c] = src[a];
                       a++;
               }
               else
               {
                       tmp[c] = src[b];
                       b++;
               }
               c++;//无论把为存在c数组中,指针都要加1
        }
        for (; a <= mid; a++, c++)//当b数组为空时,把a数组中剩下的元素都存在临时数组c中
        {
               tmp[c] = src[a];
        }
        for (; b <= end; b++, c++)//当a数组为空时,把b数组中剩下的元素都存在临时数组c中
        {
               tmp[c] = src[b];
        }
        int i;
        for (i = start; i <= end; i++)//把临时数组的元素在赋回到原来的数组中
        {
               src[i] = tmp[i];
        }
}
//归并排序
void MergeSort(int *src, int n)//给外界的接口,不是真正是排序函数
{
        int * tmp = (int *)malloc(n * sizeof(int));//申请临时空间
        dealMergeSort(src, tmp, 0, n - 1);
        free(tmp);//释放临时空间
}

五、时间复杂度与稳定性的分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值