数据结构基本排序(c语言)

  • Post author:
  • Post category:其他


在初步学完初阶数据结构的排序后,我发现了一个非常不得了的问题。那就是,在拿出一个排序的名字时(除了几个令人印象深刻的),我根本不能第一时间想起它们的思路啊!!!这就好比上厕所时,感觉要出来但又出不来一样令人难以接受。于是我准备写一篇博客来总结一下。


目录


1.冒泡排序


(1)思路


(2)代码实现


2.插入排序


(1)思路


(2)下面来举个栗子。


(3)代码实现


3.希尔排序


(1)思路


代码优化(实现):


4.直接选择排序


思路:


代码实现:


5.堆排序


思路:


画图举例:


代码实现:


6.快速排序


1,hoare版本


三数取中:(选取数组最左边数,最右边数,中间的数。三者中第二大的数)


2,挖坑法


3,前后指针法


快速排序主体(递归实现)


7.归并排序


思路(图解):


代码实现:



1.冒泡排序

首先就是我们的老伙计了,相比较其他排序老伙计算是较容易实现的了。

(1)思路

思路:

从初始位置开始进行比较,也就是将arr[0]与arr[1]进行比较。如果arr[0]大于arr[1],则将两者值进行交换。然后将arr[1]与arr[2]进行比较……..依此类推。


每次比较完一轮后,就代表比较后最大的值到达了正确的位置,这时只需要将下轮要比较的总个数-1就行了。

(2)代码实现

//冒泡排序
void BubbleSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		int flag = 0;
		for (int j = 1; j < n - i; j++)//i的逐步增加实现了下轮比较总个数的减少
		{
			if (arr[j-1] > arr[j])
			{
				swap(&arr[j - 1], &arr[j]);
				flag = 1;
			}
		}
		if (0 == flag)//当比较完一轮后,flag==0,则说明数据已经有序了
			break;
	}
}


2.插入排序

(1)思路

思路:插入排序就像打扑克摸牌一样,摸完一张牌后,就把它插到比它小的牌和比它大的牌中间。


也就是说,假设[0,end]区间内数据是有序的,接下来插入end+1位置的数。


令tmp = arr[end+1]。然后从arr[end]开始到arr[0]依次与tmp进行比较。(即end–,直到end<0或者.新入数据已插入,为止)。


如果tmp小,则将与tmp进行比较的数据向后“移”。


如果tmp大,则arr[end+1] = tmp。

(2)下面来举个栗子。

arr[end]与tmp比较,5>3,所以5向后移。end–.

同理4后移

直到2<3,tmp插入,即arr[ end+1] = tmp.

(3)代码实现

//插入排序
void insertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; ++i)//多趟
	{
        //单趟
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				--end;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}


3.希尔排序

希尔排序与插入排序思路是一样的,只是希尔排序在插排的基础上加了一层优化。(预排序)

预排序的目的呢,当然就是使数据接近有序啦。

(1)思路

那,预排序到底是什么东东呢。

其实就是将间隔为gap的数据分为一组,然后进行插入排序。

如此,较大的数据就会更快地到后面,较小的数据就会更快地到前面。

代码实现:只需要在插入排序的基础之上稍加改动就行了。

void ShellSort(int* arr, int n)
{
	for (int gap = 3; gap > 0; gap--)
	{
		for (int j = 0; j < gap; ++j)
		{
			for (int i = j; i < n - gap; i += gap)
			{
				int end = i;
				int tmp = arr[end + gap];
				while (end >= 0)
				{
					if (arr[end] > tmp)
					{
						arr[end + gap] = arr[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				arr[end + gap] = tmp;
			}
		}
	}
}

但,看着上面的代码感觉循环太多了,令人很不爽。于是就有了下面的优化。

代码优化(实现):

//希尔排序
void ShellSort(int* arr, int n)
{
	int gap = n;
	while(gap > 1)
	{
		gap = gap / 3 + 1;//使gap最终变为1
		for (int i = 0; i < n - gap; i++)//gap组数据依次多组并排 
		{
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
}

优化思路


排完红色一组的第一个数据后,i++,去排绿组的第一个数据,再i++,去排蓝组的第一个数据,再i++,去排红组第二个数据………


4.直接选择排序

思路:


其实就是将数据一个一个一个地比较,选出最小的数据,放在最左边。既然,都选出了最小的,为什么我们不再选一个最大的放在最右边呢。这么一来,新的优化思路就出现啦!

代码实现(有bug):

void SelectSort(int* arr, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		//选出小的放begin
		//选出大的放end
		int max = begin, min = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (arr[i] < arr[min])
			{
				min = i;
			}
			if (arr[i] > arr[max])
			{
				max = i;
			}
		}
		swap(&arr[begin], &arr[min]);
		swap(&arr[end], &arr[max]);
		++begin;
		--end;
	}
}

上面的代码虽然已经差不多实现出来了,但是还有一个非常容易忽略的坑。就是


当最大值正好在最左侧,min与最左侧值交换后,min就变成了所谓的“最大”。导致排序出错。


这个坑只会在同时选max,min时出现,也就是在优化的思路中出现,如果只是简单地选出min放左边并不会受到影响。

所以就需要对max进行修正。

代码实现:

//选择排序
void SelectSort(int* arr, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		//选出小的放begin
		//选出大的放end
		int max = begin, min = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (arr[i] < arr[min])
			{
				min = i;
			}
			if (arr[i] > arr[max])
			{
				max = i;
			}
		}
		swap(&arr[begin], &arr[min]);
		//修正max
		if (max == begin)
			max = min;
		swap(&arr[end], &arr[max]);
		++begin;
		--end;
	}
}


5.堆排序

堆排序与其他的排序还是有很大不同的,并且如果要理解堆排序,最最重要的方法就是画图啦。如果不画图,那就相当于把刀扔在旁边,直接用空手去劈榴莲。为了方便手不铁的我,所以接下来我就多用画图来解释了。

思路:

思路:

刚开始的数据是无序的,所以就需要将它们建成大堆或者小堆(以下使用大堆)


我们可以把父节点与子节点比较,确保最大的数位于父节点位置。

向下调整函数:

//向下调整(确保最大的数位于父节点位置)
void Adjustdown(Type* arr, int sz, int parent)
{
	int minchild = parent * 2 + 1;//初始为左孩子
	while (minchild < sz)
	{
		//取左右孩子中最大的
		if (arr[minchild] < arr[minchild + 1] && minchild + 1 < sz)
		{
			minchild++;//变为右孩子
		}
		if (arr[minchild] > arr[parent])
		{
			swap(&arr[parent], &arr[minchild]);
			parent = minchild;
			minchild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


因为叶子节点本身就是一个天然的堆,所以,只需要从倒数第一个非叶子节点(最后一个节点的父亲)开始调整。

//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)//倒数第一个非叶子节点(最后一个节点的父亲)开始调整
	{										  //由child = parent*2+1  推出parent = (child-1)/2   即  (n-1 -1)/2
		Adjustdown(arr, n, i);
	}

以下一组数据{9,1,2,4,8,6,7,5,3}为例。

(1)i = (n-1 -1)/2 = 3,所以将arr[3]与子节点比较,即4与5,3比较。因为5>4,所以4与5交换位置。

(2)i–, 所以将arr[2]与子节点比较,即2与6,7比较。因为7>6>2,所以2与7交换位置。

(3)同理,得到以下结果。

这么一来,大堆就建好啦。


然后是选数,将根节点位置的数与最后的叶子节点交换,再进行向下调整,确保根节点位置数在剩下的数中最大,最后size–,让最后一个排好的数不进入之后的排序。


每次交换使得最大的数到达正确的位置,如此反复,堆排序就完成啦!

//选数
	int i = 1;
	while (i < n)
	{
		swap(&arr[0], &arr[n - i]);//每次交换使得最大的数到达正确的位置
		Adjustdown(arr, n - i, 0);
		i++;
	}

画图举例:

交换根节点位置的数与最后叶子节点位置的数

向下调整

交换根节点位置的数与最后叶子节点位置的数

向下调整

交换根节点位置的数与最后叶子节点位置的数

向下调整

依此类推

最终实现堆排序

代码实现:

//堆排序
void Heap_sort(int* arr, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)//倒数第一个非叶子节点(最后一个节点的父亲)开始调整
	{										  //由child = parent*2+1  推出parent = (child-1)/2   即  (n-1 -1)/2
		Adjustdown(arr, n, i);
	}
	//选数
	int i = 1;
	while (i < n)
	{
		swap(&arr[0], &arr[n - i]);//每次交换使得最大的数到达正确的位置
		Adjustdown(arr, n - i, 0);
		i++;
	}
}



6.快速排序


从名字可以看出来,快速排序的主要特点就是,快!

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,之后又有一些大佬对其进行了优化,还有一些大佬又用别的思路将它实现了。所以,接下来有三种不同版本的快排实现思路。(啊啊啊,写死我啦)

1,hoare版本

思路:


总的来说就是,R找比key小的数,找到之后,R不动。


L开始行动找比key大的数,找到后,交换两数位置,R继续行动……两者相遇后将相遇位置的值与key交换。


如此,6左边全是比6小的数,右边全是比6大的数。(以图中为例)即,6到达最终的位置。再用递归来实现快排(在最后快排主体)。

因为考虑到代码效率的问题,key使用三数取中的方法来选取的效率,比直接key=arr[0]的效率高。所以,下面我就使用三数取中了哦。

三数取中:(选取数组最左边数,最右边数,中间的数。三者中第二大的数)

//三数取中
int GetMidIndex(int* arr, int left, int right)
{
	int mid = (right + left) / 2;
	if (arr[mid] > arr[left])
	{
		if (arr[mid] < arr[right])
		{
			return mid;
		}
		else if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (arr[mid] > arr[right])
		{
			return mid;
		}
		else if (arr[left] < arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

代码实现:

//快速排序hoare版本[left,right]
int PartSort1(int* arr, int left, int right)
{
	//三数取中
	int mid = GetMidIndex(arr, left, right);
	swap(&arr[mid], &arr[left]);
	int keyi = left;
	while (left < right)
	{
		//R找小
		while (left < right && arr[right] >= arr[keyi])//因为right在变化,所以要时时刻刻判断left<right
		{
			--right;
		}
		//L找大
		while (left < right && arr[left] <= arr[keyi])//因为left在变化,所以要时时刻刻判断left<right
		{
			++left;
		}
		if (left < right)
			swap(&arr[left], &arr[right]);
	}
	swap(&arr[left], &arr[keyi]);
	//返回相遇点
	return left;
}

2,挖坑法

思路:


挖坑法的思路与 hoare版本是相似的,R找比key大的数,填到左边的坑,形成新的坑位



然后L行动,找比key小的数,填到右边的坑,形成新坑位

……

两者相遇后,把key填入相遇点。

代码实现:

int PartSort2(int* arr, int left, int right)
{
	// 三数取中
	int mid = GetMidIndex(arr, left, right);
	swap(&arr[left], &arr[mid]);

	int key = arr[left];
	int hole = left;
	while (left<right)
	{
		//右边找大,填左坑
		while (left < right && arr[right] >= key)//因为right在变化,所以要时时刻刻判断left<right
		{
			--right;
		}
		arr[hole] = arr[right];
		hole = right;
		//左边找小,填右坑
		while (left < right && arr[left] <= key)因为left在变化,所以要时时刻刻判断left<right
		{
			++left;
		}
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;
	//返回相遇点
	return hole;
}

3,前后指针法

思路:


初始时,prev指针指向序列开头,cur指针指向prev指针的后一个位置。


然后判断cur指针指向的数是否小于key,若小于,则prev指针后移一位,cur指向的内容与prev指向的内容交换(prev与cur在同一位置时不变),然后cur++。


若大于,直接cur++。


当cur越界时将prev指向内容与key进行交换。

代码实现:

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	// 三数取中
	int mid = GetMidIndex(a, left, right);
	swap(&a[left], &a[mid]);

	int key = a[left];
	int prev = left;
	int cur = left + 1;
	//cur找小,prev紧跟
	while (prev <= cur)
	{
		//cur甩开prev后,并遇到小时交换
		if (a[cur] < key && ++prev != cur)
		{
			swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	swap(&a[left], a[prev]);
	return prev;
}

快速排序主体(递归实现)

思路:


首先对初始数组排序使6到达最终位置



再对6的左边数组进行排序使3到达最终位置



再对6右边数组进行排序使8到达最终位置

。再对3左边数组………最终实现有序。(二叉树结构)

//快速排序[begin,end]
void QuickSort(int* arr, int begin, int end)
{
	//为空或只有一个时返回
	if (begin >= end)
		return;
	if (end - begin <= 8)
	{
		insertSort(arr + begin, end - begin + 1);//当数组较小时,使用插入排序,提高效率
	}
	else
	{
		int keyi = PartSort1(arr, begin, end);//以上三种方法任选一个
		//左
		QuickSort(arr, begin, keyi - 1);
		//右
		QuickSort(arr, keyi + 1, end);
	}
}



7.归并排序


思路(图解):

代码实现:

//归并排序
void mergeSort(int* arr, int begin, int end, int* tmp)
{
	//分解
	//数组中数据个数为1或者0时返回
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;

	//左
	mergeSort(arr, begin, mid, tmp);
	//右
	mergeSort(arr, mid + 1, end, tmp);
	
	//合并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	//将小的放入tmp数组
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] <= arr[begin2])
			tmp[i++] = arr[begin1++];
		else
			tmp[i++] = arr[begin2++];
	}

	//将大的尾插到tmp数组
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}

	//将tmp拷贝回原来数组
	memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc");
		return;
	}

	mergeSort(arr, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;
}

啊!!!终于写完啦,最后写地脑壳疼·,如果大家发现了我哪里写错了欢迎斧正哦!文中动图来源于网络。

欧拉欧拉欧拉欧拉欧拉欧拉欧拉欧拉欧拉欧拉!!!!!

木大木大木大木大木大木大木大木大木大木大!!!!!



版权声明:本文为m0_69023444原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。