首页 > 其他分享 >C语言数据结构初阶排序(上篇)

C语言数据结构初阶排序(上篇)

时间:2024-07-15 23:25:20浏览次数:16  
标签:begin 初阶 end int C语言 gap 排序 数据结构 left

排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序。

1.插入排序

void insort(int* a, int n)//插入排序
{
	int i = 0;
	for (i= 0; i< n-1; i++)
	{
		int end = i;
		while (end>0)
		{
			if (a[end + 1] < a[end])
			{
				swap(&a[end + 1], &a[end]);
				end--;
			}
			else
			{
				break;
			}
		}
	}
}

插入排序的操作是将一条记录插入已排好序的表,从而得到一个新的,记录加1的有序表

代码解析:

因为在代码中需要end+1和end做比较,当end==n-1时,end+1便会发生越界,所以在for循环中i<n-1。

end--能够使新插入的数据不断向前比较,使其找到合适的位置,当end<0时,便会停止循环,从下一个结点继续遍历。

2.希尔排序

希尔排序有插入排序进行该着,它会在正式排序之前先对数据进行预排序,使其时间复杂度降低。

void shellsort(int* b, int n)
{
	int i = 0;
	int j = 0;
	int gap = n;
	int end = 0;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (j = 0; j < gap; j++)
		{
			for (i = 0; i < n - gap; i += gap)
			{
				end = i;
				while (end >= 0)
				{
					if (b[end + gap] < b[end])
					{
						swap(&b[end + gap], &b[end]);
						end -= gap;
					}
					else
					{
						break;
					}

				}

			}
		}
	}
}

希尔排序把数据分成gap份,然后以gap为间隔将数据区分开,从而避免插入排序面对逆序数据的最坏情况,gap会逐渐缩小,间隔也会逐渐缩小。整体的数据会更加趋于有序,这个时候使用直接插入排序效率会更高。所以gap值要不断变小。

这里gap=gap/3+1能够让gap的最后一个值刚好停在了1,当gap>1时是预排序当gap=1时,表示插入排序。

这里的j循环表示了能够区分不同组的数据,将数据分为了gap组。

这里作者使用三个循环来解决问题,在i循环中整体思路与插入排序相似,先将同一组数据先排好序,然后将切换不同组排序。

i<n-gap能够在里层的while循环的比较中避免了数组发生越界。

当排序运行到最后一次时,gap的值将变为1,在第二个for循环中将变为插入排序。

3.选择排序

选择排序是一种较为暴力的算法,它在一排数据中选出最小个的放在最左边,将最大个的放在最右边

void  selectsort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	int i = 0;
	int min=0, max=0;
	while (begin < end)
	{
		int min = begin, max = end;
		for (i = begin; i < end; i++)
		{
			if (a[i]<a[min])
			{
				min = i;
			}
			if (a[i] > a[max])
			{
				max = i;
			}
		}
		swap(&a[min], &a[begin]);
		if (max == begin)
		{
			max = min;
		}
		swap(&a[max], &a[end]);
		begin++;
		end--;
	}
}

选择排序的关键在于选出最小数和最大数的下标,然后在for循环之后将最小数和最大数的位置与初位置和末位置进行交换,但是在交换的时候需要注意更新数据的位置,不然排序的时候会发生意外。

由于是先将最小数进行交换,所以当max==begin时,begin的值已经与min的值进行互换,所以要将max=min

4.快速排序

1.hoare法

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

void qusort(int* a, int left, int right)
{
	if (left >= right)//数组不存在
	{
		return;
	}
	if ((right-left+1) < 10)
	{
		insort(a+left, right - left + 1);
	}
	else
	{
		int midi = aaa(a, left, right);
		swap(&a[midi], &a[left]);
		int begin = left, end = right;
		int key = left;
		while (begin < end)
		{
			while (begin < end && a[end] >= a[key])
			{
				end--;
			}
			while (begin < end && a[begin] <= a[key])
			{
				begin++;
			}
			swap(&a[begin], &a[end]);
		}//循环停下表示相遇
		swap(&a[begin], &a[key]);
		key = begin;
		qusort(a, left, key - 1);
		qusort(a, key + 1, right);
	}
}

 快速排序需要将数组的左区间和右区间传进函数中,如果函数的left>=right说明这个数组不存在

快速排序在面对数据量较小的时候应避免产生过多的栈帧,防止程序出现bug,所以我们在数据量较小的时候可以对数组使用插入排序。

如果数列有序,往往快速排序的时间复杂度便由O(nlog2n)退化到O(n ^2),即相当于冒泡排序,此时我们就要使用三数取中,选出一个不是最大也不是最小的数,避免end不断往下走。然后让第一位的数据与midi进行交换。

当数据量较多的时候,我们从中选出一个关键字key,比key小的数据排在它的左边,数据大的排在右边,所以在函数中应左边找大,右边找小,找出来了之后对它们进行互换。

当循环停下来的时候,表明了begin与end相遇,此时的位置数据必定比key要小

相遇场景分析:

1.begin遇end:因为函数中时右边的end先走,所以当他停下来的时候它所遇到的数据必定比key要小,然后begin继续走没有发现比key要大的数,直到遇到end。

2.end遇begin:end先走,没有遇到比key要小的数,直到遇到begin,而begin停下的位置刚好是上一轮交换的位置,此时begin位置的数据必定比key小。

这时就可以将key和begin进行位置交换,此时key左边的数都比key小,右边的数都比key大。

然后数组被分为成[left,key-1],key,[key+1,right]。此时就可以对这两个区间进行递归。

2.前后指针法 

int test2(int* a, int left, int right)
{
	int key = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur)
		{
			swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	swap(&a[key], &a[prev]);
	return prev;
}

前后指针法就是用两个下标来区分出比key要大的数,如果当cur遇到比key要小的数时,且prev++不等于cur时,此时二者之间就发生交换,prev++不等于cur是避免数组同个数据发生交换。

当cur超出right的范围时,表明数组已经遍历完成,此时将key和prev的位置发生互换便完成了排序。

3.非递归法

void qusort2(int* a, int left, int right)
{
	ss s1;
	ini(&s1);
	push(&s1, right);
	push(&s1, left);
	while (!emp(&s1))
	{
		int begin = top(&s1);
		pop(&s1);
		int end = top(&s1);
		pop(&s1);
		int key = test2(a, begin , end);
		if (key + 1 < end)
		{
			push(&s1, end);
			push(&s1, key + 1);
		}
		if (begin < key - 1)
		{
			push(&s1, key - 1);
			push(&s1, begin);
		}
	}
	del(&s1);
}

 非递归法用栈来实现,主要是模拟出递归部分,先将数组的左区间和右区间下标压入栈中,然后开始while循环,循环结束的条件是栈为空。

在栈中我们将数组的左区间和右区间的下标取出并删除栈中数据,然后使用前后指针法对整个区域进行排序,此时数组便分为[begin,key-1],key,[ket+1,end],然后对这两个子区间进行入栈,当区间元素等于1时停止入栈。

在写非递归部分时我们要注意栈的取数据的顺序。

5.归并排序

1.递归算法 

void mergetest(int* a, int* tmp,int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int midi = (left + right ) / 2;
	mergetest(a, tmp, left, midi);
	mergetest(a, tmp, midi+1, right);
	int begin1 = left, end1 = midi;
	int begin2 = midi + 1, end2 = right;
	int i = left;

	//归并
	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++];
	}
	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}
void mergesort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}
	mergetest(a, tmp, 0 , n-1);
	free(tmp);
	tmp = NULL;

}

归并排序是将两个或两个以上的有序表合并成一个有序表的过程

在算法的实现中我们先将函数递归分开,分开成长度为2的数组,然后对这个数组进行排序,如果对递归过程不太清楚的话可以画一个递归展开图。

在归并的过程中,我们分为两个区间来归并,[left,midi][midi+1,right](区间一定用这个范围,不然会造成越界问题),在这两个区间都还没碰到边界的时候对这两个数据进行比较,同时借助第三个数组tmp来完成,将较小的一个数据存放进tmp中。

当有区间已经遍历完成了以后,剩下的两个循环只会运行一个,因为这两个区间已经是有序的,所以只要将还未遍历完成的数据存放进tmp数组中就完成了。

最后用memcpy函数将tmp内已经排完的数据拷贝进原先的a数组中就可以了。

2.归并排序非递归

void mergesort2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			if (begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n-1;
			}
			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap = gap * 2;
	}
	free(tmp);
	tmp = NULL;

}

非递归的方法将数组分成gap组排序,每次循环后gap的值翻倍。

在循环的时候容易出现begin2,end2出现越界的情况,如果begin2的位置超出了数组的长度,说明没有第二个子序列需要合并,直接跳出循环,如果只有end2超出数组的长度的话那就将end2修正为正确的值。

使用memcpytmp数组中已排序的部分复制回原数组a

6.非比较排序(基数排序)

void countsort(int* a, int n)
{
	int i = 0;
	int min = 0, max = 0;
	for (i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}

		if (a[i] > max)
		{
			max = a[i];
		}
	}

	int range = max - min + 1;

	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("malloc count");
		return;
	}

	for (i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	int j = 0;
	for (i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i+min;
		}
	}

	free(count);
	count = NULL;
} 

计数排序是通过新建一个数组,对原数组的数据进行计数,最后在重新排序的算法 

在计数排序中,我们首先要算出数组的最大值和最小值,然后算出数组的range,range用来构建新的数组,避免了数组的范围不够。

count[a[i] - min]++ 的作用是将元素 a[i] 的出现次数记录在 count 数组中。每次遇到 a[i],就将对应的 count 索引的值加一。通过将原数组元素值转换为非负索引,确保可以在 count 数组中安全地记录每个元素的出现次数。

最后进行排序,与统计次数时相反,用a[j++]=i+min来排序。然后free新创建的数组就完成了

标签:begin,初阶,end,int,C语言,gap,排序,数据结构,left
From: https://blog.csdn.net/hyxdg/article/details/140309880

相关文章

  • 【C语言】指针由浅入深全方位详解
    目录指针定义指针类型野指针指针运算 指针与数组的联系二级指针 指针数组 字符指针 数组指针 数组参数,指针参数 函数指针 函数指针数组回调函数 练习题 代码仓库 指针定义1.指针是内存中一个最小单元的编号,也就是地址。2.平时口语中说的指针,......
  • 数据结构-二叉树
    引入图论中的树和现实生活中的树长得一样,只不过我们习惯于处理问题的时候把树根放到上方来考虑。这种数据结构看起来像是一个倒挂的树,因此得名。定义一个没有固定根结点的树称为无根树(unrootedtree)。无根树有几种等价的形式化定义:有n个结点,n-1条边的连通无向图无向无环......
  • 小白初识之C语言二
    重构:不增加代码功能,对代码结构的调整和优化,为了维护和拓展1.流程控制-顺序结构-分支结构(判断\选择)-循环结构2.分支-if-ifelse(三元运算替代)-ifelseifelseifelse(多路分支)-switch-case多路分支,有限,简洁3. 数组-一个标识符,存储多个值(大小是......
  • 数据结构:线性表的链式表示
    继上文《数据结构:线性表的顺序表示》,我们知道线性表的主要操作如下:InitList(&L):初始化表length(L):求表长LocateElem(L,e):按值查找操作GetElem(L,i):按位查找操作ListInsert(&L,i,e):插入操作ListDelete(&L,i,&e):删除操作PrintList(L):输出操作Empty(L):判空操......
  • C语言<<左移运算符
    在C语言中,<<是位左移运算符(BitwiseLeftShiftOperator)。这个运算符用于将一个数的各二进制位全部左移若干位,由运算符右侧的数指定移动的位数,左侧操作数的位将向左移动,移动的位数由右侧操作数决定。移动过程中,左侧操作数左侧超出位数的部分将被丢弃,而在右侧增加的部分将用......
  • 数据结构的基础(集合框架算法,复杂度和泛型)
    一.什么是集合框架        Java集合框架JavaCollectionFramework,又被称为容器container,是定义在java.util包下的一组接口interfaces和其实现类classes。        其主要表现为将多个元素element置于一个单元中,用于对这些元素进行......
  • C语言指针
    指针引用与指针引用&指针*必须初始化可以不初始化不能为空可以为空不能更换目标可以更换目标初始化案例int&r;//不合法,没有初始化引用int*p;//合法,但p为野指针,使用需要小心(1)是否需要初始化由于引用不能为空,所以我们在使用引用的时候......
  • 数据结构学习笔记——线性表
    链表1.单链表链表的插入:    (1)需要知道插入位置的前驱结点(从表头顺序遍历)    (2)先修改要插入的结点(新结点)的指针    (3)再修改前驱结点的指针链表的删除:    (1)要知道删除结点的前驱结点(从表头顺序遍历)    (2)只需要修改前驱结点的指......
  • C语言的编译和链接
    翻译环境和运⾏环境在ANSIC的任何⼀种实现中,存在两个不同的环境。第1种是翻译环境,在这个环境中源代码被转换为可执⾏的机器指令。第2种是执⾏环境,它⽤于实际执⾏代码。翻译环境那翻译环境是怎么将源代码转换为可执⾏的机器指令的呢?这⾥我们就得展开开讲解⼀下......
  • C语言中关键字static
    前言    在C语言中,static 是一个关键字,它可以用在不同的上下文中,赋予变量或函数不同的意义。static 关键字主要用于控制变量的存储期和可见性,以及函数的链接性。下面是 static 关键字的主要作用原理与用途:局部静态变量    当 static 修饰局部变量时(即......