【数据结构算法】-- 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. 稳定性:稳定

总结: 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
16进制10进制.txt 32.txt asm.txt Crctable.txt C标志符命名源程序.txt erre.txt erre2.txt ff.txt for循环的.txt list.log N皇后问题回溯算法.txt ping.txt re.txt source.txt winsock2.txt ww.txt 万年历.txt 万年历的算法 .txt 乘方函数桃子猴.txt 乘法矩阵.txt 二分查找1.txt 二分查找2.txt 二叉排序树.txt 二叉树.txt 二叉树实例.txt 二进制数.txt 二进制数2.txt 余弦曲线.txt 余弦直线.txt 傻瓜递归.txt 冒泡排序.txt 冒泡法改进.txt 动态计算网络最长最短路线.txt 十五人排序.txt 单循环链表.txt 单词倒转.txt 单链表.txt 单链表1.txt 单链表2.txt 单链表倒序.txt 单链表的处理全集.txt 双链表正排序.txt 反出字符.txt 叠代整除.txt 各种排序法.txt 哈夫曼算法.txt 哈慢树.txt 四分砝码.txt 四塔1.txt 四塔2.txt 回文.txt 图.txt 圆周率.txt 多位阶乘.txt 多位阶乘2.txt 大加数.txt 大小倍约.txt 大整数.txt 字符串查找.txt 字符编辑.txt 字符编辑技术(插入和删除) .txt 完数.txt 定长串.txt 实例1.txt 实例2.txt 实例3.txt 小写数字转换成大写数字1.txt 小写数字转换成大写数字2.txt 小写数字转换成大写数字3.txt 小字库DIY-.txt 小字库DIY.txt 小孩分糖果.txt 小明买书.txt 小白鼠钻迷宫.txt 带头结点双链循环线性表.txt 平方根.txt 建树和遍历.txt 建立链表1.txt 扫描码.txt 挽救软盘.txt 换位递归.txt 排序法.txt 推箱子.txt 数字移动.txt 数据结构.txt 数据结构2.txt 数据结构3.txt 数组完全单元.txt 数组操作.txt 数组递归退出.txt 数组递归退出2.txt 文件加密.txt 文件复制.txt 文件连接.txt 无向图.txt 时间陷阱.txt 杨辉三角形.txt 栈单元加.txt 栈操作.txt 桃子猴.txt 桶排序.txt 检出错误.txt 检测鼠标.txt 汉字字模.txt 汉诺塔.txt 汉诺塔2.txt 灯塔问题.txt 猴子和桃.txt 百鸡百钱.txt 矩阵乘法动态规划.txt 矩阵转换.txt 硬币分法.txt 神经元模型.txt 穷举搜索法.txt 符号图形.txt 简单数据库.txt 简单计算器.txt 简单逆阵.txt 线性顺序存储结构.txt 线索化二叉树.txt 绘制圆.txt 编随机数.txt 网络最短路径Dijkstra算法.txt 自我复制.txt 节点.txt 苹果分法.txt 螺旋数组1.txt 螺旋数组2.txt 试题.txt 诺汉塔画图版.txt 读写文本文件.txt 货郎担分枝限界图形演示.txt 货郎担限界算法.txt 质因子.txt 输出自已.txt 迷宫.txt 迷宫问题.txt 逆波兰计算器.txt 逆矩阵.txt 逆阵.txt 递堆法.txt 递归桃猴.txt 递归车厢.txt 递推.txt 逻辑移动.txt 链串.txt 链栈.txt 链表十五人排序.txt 链表(递归).txt 链队列.txt 队列.txt 阶乘递归.txt 阿姆斯特朗数.txt 非递归.txt 顺序栈.txt 顺序表.txt 顺序队列.txt 骑士遍历1.txt 骑士遍历2.txt 骑士遍历回逆.txt 黑白.txt

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值