【数据结构】各种排序讲解

写在前面

学到这里,基本上就将数据结构的基础部分了解完了,剩下的就是对一些算法的熟练掌握了,刷题之路就要正式开启了,每天一小步,聚沙成塔。滴水成川。


写在中间

先识结构

我们把常用的存储结构以及交换函数起来,下面的代码如果需要就直接调用即可。

//存储结构
#define MAXSIZE 10000
typedef struct
{
	int r[MAXSIZE + 1];  //用于存储要排序的数组,r[0]用于哨兵或临时变量
	int length;          //用于记录顺序表的长度
}SqList;

//交换函数封装
void swap(SqList* L, int i, int j)
{
	int temp = L->r[i];
	L->r[i] = L->r[j];
	L->r[j] = temp;
}

一、冒泡排序

  1. 简单介绍

提起冒泡排序和简单排序,我们在刚接触各种编程语言的时候就学过了,可以称得上最简单的排序算法了,至于为什么叫冒泡排序?顾名思义,排序的过程中数字就像泡泡一样慢慢的浮到数列的顶端,我们在数据结构之中再重新学习一遍,并学习冒泡排序的改进算法。

时间复杂度:1 + 2 + 3 +…+ (n-1)=n (n-1) / 2

  1. 算法步骤

5 4 3 2 1

我们拿这个倒序的数列来讲解,将其正序排列(重在理解哈)

首先将5和4比较,5>4,交换这两个位置上的元素,此时数列为4 5 3 2 1

其次将5和3比较,5>3,交换这两个位置上的元素,此时数列为4 3 5 2 1

接着将5和2比较,4>2,交换这两个位置上的元素,此时数列为4 3 2 5 1

然后将5和1比较,4>1,交换这两个位置上的元素,此时数列为4 3 2 1 5

这是第一趟排序,排序之后5这个元素就排在了数列的最后,之后通过循环就可以进行第二趟排序,4就排在了倒数第二位;第三趟排序后,3就排在了倒数第三位,第四趟排序后,2就排在了倒数第二位。至此数列为1 2 3 4 5,Over !

  1. 代码实现
//这段代码是从前两个元素开始进行比较,交换的
void BubbleSort(SqList* L)
{
	int i, j;
	for (i = 0; i < L->length-1; i++)
	{
		for (j = 0; j <= L->length-1-i; j++)
		{
			if (L->r[j] > L->r[j + 1])
			{
				swap(L, j, j + 1);
			}
		}
	}
}

//换一种写法也要会,从最后两个元素开始比较交换
void BubbleSort(SqList* L)
{
	int i, j;
	for (i = 1; i < L->length; i++)
	{
		for (j = L->length - 1; j >= i; j--)
		{
			if (L->r[j] > L->r[j + 1])
			{
				swap(L, j, j + 1);
			}
		}
	}
}

//改进写法要看懂
void BubbleSort_new(SqList* L)
{
	int i, j;
	int flag = 1;                        //用flag来标记
	for (i = 1; i < L->length && flag; i++) //若flag为1,则有数据交换,否则退出循环
	{
		flag = 0;                           //初始flag置为0
		for (j = L->length - 1; j >= i; j--)
		{
           if(L->r[j] > L->r[j+1])
           { 
			  swap(L, j, j + 1); 
			  flag = 1;                      //如果有数据交换,则flag置1
           }
		}
	}
}

二、简单选择排序

  1. 简单介绍

冒泡排序的思想就是通过不断的交换来完成最终的排序,那么能不能在排序时找到合适的关键字后再交换,并且只移动一次就完成相应关键字的排序定位呢?别着急,选择排序就可以很好的满足上面的的需求。

时间复杂度:1 + 2 + 3 +…+ (n-1)=n (n-1) / 2

  1. 算法步骤

5 4 3 2 1

我们同样拿这个倒序的序列来讲解,将其正序排列。

  • 第一趟排序 5 4 3 2 1

    首先将5和4比较,5>4,交换这两个位置上的元素,此时数列为4 5 3 2 1

    其次将4和3比较,4>3,交换这两个位置上的元素,此时数列为3 5 4 2 1

    接着将3和2比较,3>2,交换这两个位置上的元素,此时数列为2 5 4 3 1

    最后将2和1比较,2>1,交换这两个位置上的元素,此时数列为1 5 4 3 2

  • 第二趟排序 1 5 4 3 2

    首先将5和4比较,5>4,交换这两个位置上的元素,此时数列为1 4 5 3 2

    其次将4和3比较,4>3,交换这两个位置上的元素,此时数列为1 3 5 4 2

    最后将3和2比较,3>2,交换这两个位置上的元素,此时数列为1 2 5 4 3

  • 第三趟排序 1 2 5 4 3

    首先将5和4比较,5>4,交换这两个位置上的元素,此时数列为1 2 4 5 3

    接着将5和3比较,5>3,交换这两个位置上的元素,此时数列为1 2 3 5 4

  • 第四趟排序

    将5和4比较,5>4,交换这两个位置上的元素,此时数列为1 2 3 4 5

  1. 代码实现
void SelectSort(SqList* L)
{
	int i, j, min;
	for (i = 0; i < L->length-1; i++)
	{
		min = i;
		for (j = i + 1; j <= L->length; j++)
		{
			if (L->r[min] > L->r[j])
				min = j;
		}
        //遍历一遍之后,此时min记录的是最小值下标
		if (i != min)
			swap(L, i, min);
	}
}

三、直接插入排序

  1. 简单介绍

直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的长度增1的有序表,算法思想就像打扑克牌时的发完牌后的理牌阶段,选定一张牌,将小牌放左边,大牌放右边。

时间复杂度:最坏 2 + 3 +…+ n = (n+2) (n-1) / 2,总:O(n^2)

  1. 算法步骤

0 5 3 4 6 2

实际代码中,我们将上面的6个数存进数组r[6]中,r[0]相当于一个临时存储区,我们起一个好听的名名字——哨兵,权且赋初值为0,哨兵参与排序,不参与输出,下面将上面的5个数字升序排列,

  • 首先将 r[2] 和 r[1] 比较,即3<5,应放在5 的左边。将3存进r[0]中,5移到原来3的位置上,r[0]移到原来5的位置上,此时数组为3 3 5 4 6 2

  • 接着将 r[3] 和 r[2] 比较,即4<5,应放在5左边。我们将4存进r[0]中,5移到原来4的位置上,r[0]移到原来5的位置,此时数组为4 3 4 5 6 2

  • 其次将 r[4] 和 r[3] 比较,即6>5,6本就在5的右边,无需进行交换。

  • 最后将 r[5] 和 r[4] 比较,即2<6,应将2放在6的左边,将2存进r[0]中,由于6 5 4 3都比2大,它们都将向右移动一位,将2放到当前3的前面。此时数组为2 2 3 4 5 6

  1. 代码实现
void InsertSort(SqList* L)
{
	int i, j;
	for (i = 2; i < L->length; i++)
	{
		if (L->r[i-1] > L->r[i])
		{
			L->r[0] = L->r[i];                       //将要插入的值暂存在r[0]
			for (j = i - 1; L->r[j] > L->r[0]; j--)  //只要i前面的数比要插入的数大,就一直循环,将前面的大数向后移动一位
				L->r[j + 1] = L->r[j];
			L->r[j + 1] = L->r[0];
		}
	}
}

四、希尔排序

  1. 简单介绍

上面介绍了直接插入排序,在数列有序且数据较少时,直接插入的效率很高,问题在于这两个条件过于苛刻,没有条件,我们就去创建条件,希尔排序不就来了么!思想就是将原来的大量数据进行分组。分割成若干子序列(这样每个子序列中的数据就少了),在这若干个子序列中分别进行直接插入排序。

  1. 算法步骤

[算法]六分钟彻底弄懂希尔排序,简单易懂_哔哩哔哩_bilibili

由于这个算法文字解释起来有点麻烦,就奉上一个比较好的讲解视频。同时来举个例子

9 1 5 8 3 7 4 6 2,数组长度为9,我们设置步长为4

d=4,9 1 5 8 3 7 4 6 2 ,分组后组内进行直接插入排序,结果为3 1 4 6 9 7 5 8 2

d=2,3 1 4 6 9 7 5 8 2 ,分组后组内进行直接插入排序,结果为2 1 3 6 4 7 5 8 9

d=1,2 1 3 6 4 7 5 8 9 ,分组后组内进行直接插入排序,结果为1 2 3 4 5 6 7 8 9

  1. 代码实现
void ShellSort(SqList* L)
{
	int i, j, k = 0;
	int increment = L->length;
	do
	{
		increment = increment / 3 + 1;
		for (i = increment + 1; i <= L->length; i++)
		{
			if (L->r[i] < L->r[i - increment])
			{
				L->r[0] = L->r[i];
				for (j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment)
					L->r[j + increment] = L->r[j];
				L->r[j + increment] = L->r[0];
			}
		}
	} while (increment > 1);
}

五、堆排序

  1. 简单说明

前面我们介绍了简单选择排序,它在待排序的n个记录中选择一个最小的记录 ,需要比较n-1次。但是这样的操作没有把每一趟的比较结果保存下来,在最后一趟的比较之中,有许多比较在前一趟已经做过了,因此记录的比较次数较多。而堆排序就是对简单选择排序进行的一种改进。

实际上堆是具有以下性质的完全二叉树:

  • 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;

  • 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

  1. 算法思想

数据结构——堆排序_哔哩哔哩_bilibili

  1. 代码实现
void HeapSort(SqList* L)
{
	int i;
	for (i = L->length / 2; i > 0; i--)
	{
		HeapAdjust(L, i, L->length);
	}
	for (i = L->length; i > 1; i--)
	{
		swap(L, 1, i);
		HeapAdjust(L, 1, i - 1);
	}
}

void HeapAdjust(SqList* L, int s, int m)
{
	int temp, j;
	temp = L->r[s];
	for (j = 2 * s; j <= m; j *= 2)
	{
		if (j < m && L->r[j] < L->r[j + 1])
			++j;
		if (temp >= L->r[j])
			break;
		L->r[s] = L->r[j];
		s = j;
	}
	L->r[s] = temp;
}、快速排序

六,快速排序

  1. 简单介绍

芜湖,接下来介绍最后一个排序,快速排序是我们前面认为最慢的冒泡排序的升级,它们都属于交换类排序。基本思想是通过一趟排序将待排记录分割成独立的两个部分,其中一部分记录的关键字比另一部分记录的关键字小,则可分别对这两部分记录继续排序,以达到整个序列有序的目的。

  1. 算法步骤

50 10 90 30 70 40 80 60 20

我们拿上面的数列来举例,枢轴 pivotkey

  • pivotkey=50,low=50,high=20,由于50>20,交换两者位置,此后low指向10,

    此时数列为20 10 90 30 70 40 80 60 50

  • pivotkey=50,low=10,high=50,由于10<50,无需交换,此后low指向90

    此时数列为20 10 90 30 70 40 80 60 50

  • pivotkey=50,low=90,high=50,由于90>50,交换两者位置,此后high指向60

    此时数列为20 10 50 30 70 40 80 60 90

  • pivotkey=50,low=50,high=60,由于50<60,无需交换,此后high指向80

    此时数列为20 10 50 30 70 40 80 60 90

  • pivotkey=50,low=50,high=80,由于50<80,无需交换,此后high指向40

    此时数列为20 10 50 30 70 40 80 60 90

  • pivotkey=50,low=50,high=40,由于50>40,交换两者位置,low指向30

    此时数列为20 10 40 30 70 50 80 60 90

  • pivotkey=50,low=30,high=50,由于30<50,无需交换,此后low指向70

    此时数列为20 10 40 30 70 50 80 60 90

  • pivotkey=50,low=70,high=50,由于70>50,交换两者位置,high指向50

    此时数列为20 10 40 30 50 70 80 60 90

当low和high重合的时候,快速排序的第一趟排序就完成了,这样枢轴值50就到达了正确的位置,通过递归调用,将剩余的部分分成两个块,选定新的枢轴值,后面的排序和本次排序的方法基本一致,这里不在赘述了

  1. 代码实现
//快速排序
//对顺序表L做快速排序
void QuickSort(SqList* L)
{
	QSort(L, 1, L->length);
}

//对顺序表L中的子序列L->r[]做快速排序
void QSort(SqList* L,int low,int high)
{
	int pivot;
	if (low < high)
	{
		//将顺序表一分为二,算出枢轴值pivot
		pivot = Partition(L, low, high);
		QSort(L, low, pivot - 1);    //对低子表递归排序
		QSort(L, pivot + 1, high);   //对高子表递归排序
	}
}

int Partition(SqList* L, int low, int high)
{
	int pivotkey;
	pivotkey = L->r[low];     //用子表的第一个位置做枢轴记录
	while (low < high)        //从表的两端交替的向中间扫描
	{
		while (low < high && L->r[high] >= pivotkey)
			high--;
		swap(L, low, high);   //将比枢轴记录小的记录交换到底端
		while (low < high && L->r[low] <= pivotkey)
			low++;
		swap(L, low, high);   //将比枢轴记录大的记录交换到高端
	}
	return low;               //返回枢轴所在的位置
}


写在最后

上面的代码难免会有疏漏,如果各位大佬发现错误,请一定要指正

点赞,你的认可是我创作的动力!
⭐ 收藏,你的青睐是我努力的方向!
✏️ 评论,你的意见是我进步的财富!

你可能感兴趣的:(数据结构,数据结构,算法,排序算法)