【数据结构算法】-- C语言

用C语言实现的数据结构算法,下面来一个一个讲解:

(Swap函数在末尾,一个换位函数,理解即可)

1,插入排序

顾名思义就是一个值从前面开始一个一个插入,插入的时候排序一次,有 n 个数就排序 n 次

思想简单所以不介绍,代码如下:

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];

		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;
		}
		a[end + 1] = tmp;
	}
	
}

直接插入排序的特性总结:

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

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

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

4. 稳定性:稳定 (稳定指相同的值,后值不会排到前值之前)

2,希尔排序

详细了解可以看我的前一篇文章,希尔排序就是相隔 gap 个距离的数值做排序,使其越来越接近有序,最后以间隔为 1 的 gap 小幅度排序结束。

 代码如下:

void ShellSort(int* a, int n)
{
	int gap = n / 3 + 1; //根据数组长度分大小
	while (gap > 1) //最后一次跳出
	{
		for (int i = 0; i < n - gap; i++)  //用i++可以巧妙地运用多组
		{
			int end = i;
			int tmp = a[end + gap];  //插入的数值,间隔gap个
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;  //往前比较
				}
				else
					break;
			}
			//当它最小或者遇到比它大的值时,赋值当前位置
			a[end + gap] = tmp;

		}
		gap = gap / 3 + 1;  //逐渐收缩

		for (int i = 0; i < n; i++)
		{
			printf("%d ", a[i]);
		}
		printf("\n");
	}

	//最后一次移动
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + gap] = a[end];
				end -= 1;  //往前比较
			}
			else
				break;
		}
		//当它最小或者遇到比它大的值时,赋值当前位置
		a[end + 1] = tmp;
	}

}

希尔排序的特性总结:

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

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就可以达到优化的效果

3. 时间复杂度: O(N^1.3—N^2),不好计算,需要推导

4. 空间复杂度:0(1)

5. 稳定性:不稳定

3,选择排序

选择排序的思想是:

首先遍历数组,找出最大值和最小值,分别赋予给头和尾,随后头++,尾--,再次遍历数组,换头换尾,遍历,换头换尾,直到头尾指针相遇。 

 

代码如下:

void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;

	while (left < right)
	{
		int minsort = left, maxsort = left;
		for (int i = left; i <= right; i++)
		{
			if (a[i] > a[maxsort])
			{
				maxsort = i;
			}
			else if (a[i] < a[minsort])
			{
				minsort = i;
			}
		}
		Swap(&a[left], &a[minsort]);
		if (maxsort == left)  //判断避免max赋予了min的值
		{
			maxsort = minsort;
		}
		Swap(&a[right], &a[maxsort]);
		left++;
		right--;
	}

}

直接选择排序的特性总结:

1. 选择排序非常好理解,但是效率不是很好,因为要一直遍历

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

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

4. 稳定性:不稳定,选择排序其实是不稳定的

4,堆排序

大小堆排序也很好理解,升序建大堆,降序建小堆,只要不破坏其父子关系,把头和尾交换后--n即可

 

 代码如下:

//堆排序-子
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;

	while (child < n)
	{
		if (child < n - 1 && a[child + 1] > a[child])
		{
			child++;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
	
}
//堆排序-母
void HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1)/2; i >= 0; i--)
	{
		//建堆
		AdjustDown(a, n, i);
	}
	//已经建好
	while (n > 0)
	{
		Swap(&a[0], &a[n - 1]);
		AdjustDown(a, n - 1, 0);
		n--;
	}

}

直接选择排序的特性总结:

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

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

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

4. 稳定性:不稳定 

5,冒泡排序

冒泡排序的思想是:

从头到尾每一位数都来一次遍历,把最大值(最小值)放在最后,随后隐藏掉即可。

冒泡排序是一种运气排序,如果后面是有序,则不用继续遍历下去,如果是无序的 ,则需要一个一个来,比插入排序的时间复杂度还久,选用原因是:代码简单啊

 代码如下:

//冒泡排序
void Bubble(int* a, int n)
{
	for (int i = n - 1; i >= 0; i--)
	{
		int count = 0;
		for (int j = 0; j < i; j++)
		{
			if (a[j] > a[j + 1])
			{
				int tmp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = tmp;
				count = 1;
			}
		}
		if (count == 0)
			break;
	}
}

冒泡排序的特性总结:

1. 容易理解

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

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

4. 稳定性:稳定

6,快速排序

本次快排有3种,都是常用到的:

1. hoare版本   2. 挖坑法   3. 前后指针版本

第一种:hoare快排

 hoare的思想:

首先定义 left 和 right 、key 指针,left 和 right 分别表示左和右,key 代表比较值,关键点:如果选 left 为 key,就要让 right 指针先走,如果选 right 作为 key ,反之。

right 先走,走到比 key 小的位置停下,到 left 走,走到比 key 大的位置停下,然后 a[left]  与 a[right] 互换,right 继续走,重复,直到 right 与 left 相交的时候,把相交的位置 meet 与 key 所在的值交换,给到的 meet 值可以作为递归的 left 和 right。

代码如下:

int QuickHoare(int* a, int left, int right)
{
	//三数取中法
	int mid = GetKey(a, left, right);
	Swap(&a[left], &a[mid]);

	int key = left;

	while (left < right)
	{
		//右边先移
		while (left < right && a[right] >= a[key])
		{
			right--;
		}
		//再到左边移动
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);

	}
	Swap(&a[key], &a[left]);
	//给到中间来
	int meet = left;

	return meet;

}

递归的函数一齐放到快排的最后。

第二种:挖坑法

挖坑法的思想:

left right hole 指针,hole作为坑,首先取出 left 的值赋予 hole,随后 right 先走,到比 hole 大的位置停下,a[right] 直接 赋予 a[left] 上,因为一开始 left 所在的值给了 hole ,所以不用担心被覆盖。随后 left++,遇到比 hole 小的就停下,a[left] 赋予 a[right] 上,随后right--,赋值,left++,赋值,知道两指针相交的位置 meet ,把 hole 的值给到 meet 上。

 代码如下:

int QuickDig(int* a, int left, int right)
{
	//挖坑
	int hole = a[left];

	//排序
	while (left < right)
	{
		//先走右
		while (left < right && a[right] >= hole)
			right--;
		a[left] = a[right]; //填坑

		while (left < right && a[left] <= hole)
			left++;
		a[right] = a[left]; //填坑

	}
	a[left] = hole;
	int meet = left;

	return meet;

}

 第三种:前后指针法

前后指针法的思想:

给一个 key prev cur 指针,cur = prev + 1 ,key 存放 prev 所在的值,如果 cur 的值小于 key 的值,则 cur 与 prev 的+1互换,如果 cur 遇到大于 key 的值,则一直++,直到 cur = right,随后 key 与 prev 互换,prev 作为 meet 递归下去。

代码如下:

int QuickPoint(int* a, int left, int right)
{
	int prev = left, cur = prev + 1;
	//三数取中
	//int mid = GetKey(a, left, right);
	//Swap(&a[mid], &a[prev]);
	int key = prev;

	while (cur <= right)
	{
		if (a[key] > a[cur] && ++prev != cur) //避免原地TP
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[key], &a[prev]);

	return prev;
}

优化代码 -- 三数取中法的代码: 

int GetKey(int* a, int left, int right)
{
	int mid = (left + right) >> 1; // 两值/2
	if (a[left] < a[mid]) // 看 left   mid   right 哪个在中间 
	{
		//left < mid ? right
		if (a[mid] < a[right])
			return mid;
		else if (a[left] > a[right])
			return left;
		else
			return right; // left < right < mid
	}
	else // (a[left] > a[mid])
	{
		//mid < left ? right
		if (a[mid] > a[right])
			return mid;
		else if (a[left] < a[right])
			return left;
		else
			return right;
	}
}

 递归的代码:

如果需要优化,则可以使用 三数取中法 和 分治递归

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	int keyi = QuickPoint(a, begin, end);

	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);

	 1、如果这个子区间是数据较多,继续选key单趟,分割子区间分治递归
	 2、如果这个子区间是数据较小,再去分治递归不太划算
	//if (end - begin > 20)
	//{
	//	int keyi = QuickPoint(a, begin, end);

	//	// [begin, keyi-1] keyi [keyi+1, end]
	//	QuickSort(a, begin, keyi - 1);
	//	QuickSort(a, keyi + 1, end);
	//}
	//else
	//{
	//	//HeapSort(a + begin, end - begin + 1);
	//	InsertSort(a + begin, end - begin + 1);
	//}
}

 快速排序的特性总结:

1. 你可以永远相信快排,快排yyds!

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

3. 空间复杂度:O(logN)

4. 稳定性:不稳定

7,非递归快排

非递归快排需要用到栈,利用栈的原理,类似于递归,假如区间是0-10,第一次放进0,10;第二次放0,5,6,10;第三次放0,5,6,8,9,10。。。每一次取 meet 值,都已经排好序了,随后[6,10]已经排好,开始[0,5]的区间,先放0,2,3,5;再到0,2,3,4,5,随后取出4,5作为左右区间排序,直到排好,这里我们需要 malloc 一个空间存放排序后的值,最后再复制给原数组,free掉。

代码如下:

void QuickSortNonR(int* a, int left, int right)
{
	stack st;
	StackInit(&st);
	//先存左右区间
	StackPush(&st, left);
	StackPush(&st, right);
	//空了就代表排序完成,没空继续
	while (!StackEmpty(&st))
	{
		int end = StackTop(&st);
		StackPop(&st);

		int begin = StackTop(&st);
		StackPop(&st);

		int key = QuickHoare(a, begin, end);

		if (begin < key - 1)
		{
			StackPush(&st, begin);
			StackPush(&st, key - 1);
		}

		if (end > key + 1)
		{
			StackPush(&st, key + 1);
			StackPush(&st, end);
		}

	}

	StackDistroy(&st);
}

8,归并排序

归并排序的思想:

归并排序实质上是一个一直分治的过程,它把原数组拆分成小份比较排序,这样使得,每一组比较的数组都是有序的,只需要创建两个指针,left 代表第一组的头,right 代表第二组的头,创建一个空间,谁小就先放进去,如果哪个数组先结束,另外一组就直接加进队尾,因为是有序的,最后复制到原数组上去,free掉。

代码入下:

//归并操作
void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{

	int i = begin1, j = i;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	for (; j <= end2; j++)
	{
		a[j] = tmp[j];
	}
}

//子函数
void _MergeSort(int* a,int *tmp, int left, int right)
{
	if (left >= right)
		return;

	int mid = (left + right) >> 1;
	_MergeSort(a, tmp, left, mid);
	_MergeSort(a, tmp, mid + 1, right);

	int begin1 = left, begin2 = mid + 1, end1 = mid, end2 = right;
	_Merge(a, tmp, begin1, end1, begin2, end2);
}

//归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc false");
		return;
	}

	int left = 0; int right = n - 1;
	_MergeSort(a, tmp, left, right);


	free(tmp);
}

归并排序的特性总结:

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

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

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

4. 稳定性:稳定

9,非递归归并排序

非递归归并的话要求就多了,先看代码:

//归并操作
void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{

	int i = begin1, j = i;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	for (; j <= end2; j++)
	{
		a[j] = tmp[j];
	}
}

//非递归归并排序
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc false");
		return;
	}
	int left = 0, right = n - 1;
	int gap = 1;   //间隔为 1 ,再往后递增

	while (gap < n)
	{
		for (int j = 0; j < right; j += gap*2)
		{
			//[j, j+gap-1] [j+gap, j+gap*2-1]
			int begin1 = j, begin2 = j + gap, end1 = j + gap - 1, end2 = j + gap * 2 - 1;
			//考虑多种情况
			//1,前区间结尾小于right 和 只有前区间,没到后区间
			if (end1 >= right)
				break;
			//2,有前区间,但后区间小于right的
			if (end2 > right)
			{
				end2 = right;
			}

			_Merge(a, tmp, begin1, end1, begin2, end2);

		}

		gap *= 2;
	}

}

这里复杂的是 _Merge函数 前后两个区间的取值,我们需要注意不可以让 end1 或者 end2 越界,间距gap 每次 *=2,令 j = gap*2,考虑3种状态,有可能到一半就到头了,后半身还没进去,图中1和3条件可以一样,因为都没有后区间,干脆不执行。

10,计数排序

 计数排序其实很简单,它涉及到一个映射问题

计数排序的思想:

创建一个数组,该数组的下标对应的是要排序数组的值,比如上图,data 中 4就放在下标为 count 中4的下标,这时候该下标+1,如果有3个6,count 的[6]就等于3,代表6有3个。同样的道理,1就放1,2就放2,放一个就+1个,最后按照下标 count[6] = 3的值打印3个6。

由于是绝对映射,所以可能存在空间浪费,比如 data 最小值为1000,最大值为1001,难道要创建[0,1001]吗?!所以这里需要用到相对映射,先遍历一遍数组,找到最大值和最小值,最小值就是映射的值,后面的加减只需要 +- 最小值min即可。

代码如下:

void CountSort(int* a, int n)
{
	int gap = 0, min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}
	//求出最大值和最小值的差,相对映射
	gap = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * gap);
	memset(count, 0 ,sizeof(int) * gap);
	
	//标记到count上
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	//赋值回去
	int i = 0;
	for (int j = 0; j < gap; j++)
	{
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}

	free(count);

}

计数排序的特性总结:

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限

2. 时间复杂度:O(MAX(N,范围))

3. 空间复杂度:O(范围)

4. 稳定性:稳定

总结: 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值