数据结构【内排序(本节为升序)】

本文详细介绍了各种排序算法,包括直接插入排序、希尔排序、选择排序(包括堆排序)、交换排序(冒泡排序和快速排序的多种实现)以及归并排序(递归和非递归)。还探讨了每种算法的时间复杂度、空间复杂度和稳定性特点。
摘要由CSDN通过智能技术生成

目录

一.插入排序

1.直接插入排序

2.希尔排序

二.选择排序

1.选择排序

2.堆排序

三.交换排序

1.冒泡排序

2.快速排序(递归)

①.hoare法

②.挖坑法

③.前后指针法

3.快速排序(非递归)

四.归并排序

1.归并排序(递归)

 2.归并排序(非递归)

五、计数排序


一.插入排序

1.直接插入排序

直接插入排序就和打扑克的时候,我们一张张整理牌的过程。

此时左边是我们手上已经整理好的牌,现在又摸了一张J,需要将J插入进去,再使左边牌有序,

直接插入排序就是这种思路。

来分析分析一趟排序:

 

arr[end]>5,将arr[end]后移,end--,此时end<0,已经没有再小的数了,即5就是最小数,在arr[end+1]处插入5。 

这就是一趟排序的逻辑。

用代码实现:

int end = 0;              //有序区间【0-0】
		int tmp = arr[end + 1];   //待排序的数

		while (end >= 0)          
		{
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];        //往后挪
				end--;
			}
			else
				break;
		}
		arr[end+1] = tmp;        
	}

然后再来想有N个数,就得走N次排序,外层循环就很简单了 

 void Insertsort(int* arr, int sz)        //插入排序
{
	for (int i = 0; i < sz - 1; i++)    //控制边界,防止越界
	{
		int end = i;              //有序区间
		int tmp = arr[end + 1];   //待排序的数

		while (end >= 0)          
		{
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];          //往后挪
				end--;
			}
			else
				break;
		}
		arr[end+1] = tmp;        
	}
	
}

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

空间复杂度: O(1)

稳定性:稳定

2.希尔排序

希尔排序在我看来就是优化版的直接插入排序,相比与直接插入排序,希尔排序多了一个预排序,可以让数组更快的有序。

每间隔为gap的数为一组,分组排序。

第一趟排序

第二趟排序

 gap为1时,就是直接插入排序。

void shellsort(int* arr, int sz)  
{
	int gap = sz;
	int i = 0;
	while (gap > 1)
	{
		gap /= 2;
		for ( i = 0; i < sz - gap; i++)    //注意越界问题
		{
			int end = i;
			int tmp = arr[end + gap];          //待排序的数
			while (end >= 0)
			{
				
				if (tmp < arr[end])
				{
					arr[end + gap] = arr[end];      //往后挪
					end -= gap; 
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;
		}
		
	}
}

时间复杂度:O(N^1.3)

空间复杂度: O(1)

稳定性:不稳定

二.选择排序

1.选择排序

每次排序选出最小/最大/最小最大,将最小数往前插,最大数往后插

	void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void selectsort(int* arr, int sz)
{
	int left = 0;
	int right = sz - 1;
	while (left < right)
	{
		int maxi = right;
		int mini = left;
		for (int i = left; i <= right; i++)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
		}

		swap(&arr[left], &arr[mini]);
		if (left == maxi)
		{
			maxi = mini;
		}
		swap(&arr[right], &arr[maxi]);
		left++;
		right--;

	}


}

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

空间复杂度: O(1)

稳定性:不稳定

2.堆排序

堆排序利用了大堆/小堆的特性,但由于这一组数本身不是大小堆,所以我们要向下/向上调整建堆,使这组数变成一个大/小堆。

void adjustdown(int*arr,int sz,int parent)   // 数组,数据个数,父节点
{
	int child = parent * 2 + 1;
	while (child < sz)
	{
		if (child + 1 < sz && arr[child] < arr[child + 1]) 
            //child + 1 < sz  —— 防止越界
		{
			child++;
		}
		if (arr[parent] < arr[child])
		{
			swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}
}

建完堆后的样子:

然后把根结点的数据(20)和最后一个结点的数据(8)交换,此时堆又变成了乱序,又需要我们重新调整建堆,但这次建堆的范围少了1,因为最大的数我们已经换到了最后,简而言之,已经排序完成了一个数。

这就是一趟排序,所以完整排序就很简单了

void adjustdown(int*arr,int sz,int parent)   // 数组,数据个数,父节点
{
	int child = parent * 2 + 1;
	while (child < sz)
	{
		if (child + 1 < sz && arr[child] < arr[child + 1]) //child + 1 < sz  —— 防止越界
		{
			child++;
		}
		if (arr[parent] < arr[child])
		{
			swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}
}

void heapsort(int* arr, int sz)
{
	//升序,建大堆

	for (int i = (sz - 1 - 1) / 2; i >= 0; i--) // 建堆 从最后一个根节点开始调整 
		//【因为不满足向上/向下调整的条件】从下至上一个个调整。
	{
		adjustdown(arr, sz, i);    //数组 元素个数 父节点
	}

	int end = sz - 1;
	while (end > 0)
	{
		//把最大的甩在最后一个叶子结点,变乱序
		swap(&arr[0], &arr[end]);
		//再向下调整成大堆
		adjustdown(arr, end, 0);              //end为总个数 会变化
		end--;
	}
	
}

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

空间复杂度:O(1)
稳定性:不稳定

三.交换排序

1.冒泡排序

每次相邻两个数进行比较,小的数往前换,大的数往后换,每一轮冒泡完毕后,最大的数会沉到最后。

void BubbleSort(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)        //N个数,走N次
	{
		for (int j = 1; j < sz - i; j++)      //每一轮确定一个最大数
		{
			if (arr[j] < arr[j - 1])
			{
				swap(&arr[j], &arr[j - 1]);
			}
		}
	}
}
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定

2.快速排序(递归)

快速排序:取数中某一个位基准值,按照基准值将数据分为两个区间,左区间小于基准值,右区间大于基准值,一直重复此过程,直至排序完成。

对于对基准值key的选择,可以是选择每个区间的任意一个数,但是选择不同,可能会影响排序的效率。

1.常规选每个区间的第一个数,此方法若是遇见数据有序的情况,效率会非常低,时间复杂度低至O(N^2)。

2.在区间随机选key,此方法对方法1优化了一点,但由于是随机选择,可能也会出现1的情况

3.三数取中,此方法比较稳定,能每次较平均的划分左右区间,但对于大量重复数据,仍具有不可处理的情况。

为了方便,本文采取方法1来做示例

①.hoare法

该方法的思路:


right从右至左找小于key的数,left 再从左往右找大于key的数,找到后交换left 和right 指向的数。right,left指针再找,直到两个指针相遇。最后再将key与相遇的位置的数交换,一轮排序完成。

 此时一轮排序已成,

再次分出左右区间,继续排序。

void Quicksort(int* arr, int left, int right)
{


	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;

	int keyi = left;

	while (left < right)
	{
		while (left < right&&arr[right] >= arr[keyi])
		{
			right--;          //找小
		}
		while (left < right&&arr[left] <= arr[keyi])
		{
			left++;          //找大
		}
		swap(&arr[left], &arr[right]);
	}

	swap(&arr[left], &arr[keyi]);           //交换基准值和指针相遇位置的数
	keyi = left;

	Quicksort(arr, begin, keyi - 1);           //递归左区间
	Quicksort(arr, keyi+1, end);               //递归右区间
}

②.挖坑法

思路:

取基准值key,把key的位置当做一个坑,right指针从右往左找小,找到后放入坑,right位置处形成新的坑,left指针再从左往右找大,找到后放入坑,left位置形成新的坑,如此循环,直到left,right相遇,将基准值key放入坑。一轮排序结束。

 再递归分左右区间继续排序,

void Quicksort(int* arr, int left, int right)

{
	if (left >= right)
	{
		return;
	}

	int key = arr[left];
	int keyi = left;
	int begin = left;
	int end = right;

	while (left < right)
	{
		while (left < right&&arr[right] >= key)
		{
			right--;
		}
		arr[keyi] = arr[right];
		keyi = right;

		while (left < right&&arr[left] <= key)
		{
			left++;
		}

		arr[keyi] = arr[left];
		keyi = left;
		
	}
	arr[keyi] = key;
	 
	Quicksort(arr, begin, keyi-1);
	Quicksort(arr, keyi+1, end);

}

③.前后指针法

思路:

取基准值key,perv指针指向序列开头cur指针指向prev下一个位置,若cur指针指向的数大于key,cur往后走,若cur指针指向的数小于key,prev往后走,如果prev!=cur,则交换prev和cur的值cur往后走,反之,cur往后走。直到cur为空交换perv指针指向的数和key

void Quicksort(int* arr, int left, int right)
{
	

	int keyi = left;


	int prev = left;
	int cur = left + 1;
	while (cur<=right)
	{
		if (arr[cur] > arr[keyi])
		{
			cur++;
		}
		else
		{
			prev++;
			if (prev != cur)
			{
				swap(&arr[prev], &arr[cur]);
			}
			cur++;
			
		}
	}
	swap(&arr[prev],&arr[keyi]);
	keyi = prev;

	Quicksort(arr, begin, keyi-1);
	Quicksort(arr, keyi+1, end);



}

3.快速排序(非递归)

用栈实现,把需要排序的区间入栈,排完了就出栈。

void QuicksortNonR(int* arr, int left, int right)
{
	ST st;
	StackInit(&st);
	StackInsert(&st, right);
	StackInsert(&st, left);
	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);

		int mid = Quicksort(arr, begin, end);        //需要改造一下
         
		if (mid+1 < end)   //还有至少2个数,继续进,否则结束
		{
			StackInsert(&st, right);
			StackInsert(&st, mid+1);
		}
		if (begin < mid - 1)
		{
			StackInsert(&st, mid-1);
			StackInsert(&st, begin);
		}
	}
	StackDestroy(&st);
}
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定

四.归并排序

1.归并排序(递归)

思路:

类似于二叉树的后序遍历,先排序子树,再排序根,将左右子树分割成一个不可再分割的子问题,即只有一个数。

void _MergeSort(int* arr, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return;
	}

	int mid = (left + right) / 2;
	_MergeSort(arr, left, mid,tmp);              //分别找左右各一个比较
	_MergeSort(arr, mid+1,right,tmp);

	int begin1 = left, end1 = mid;
	int begin2 = mid+1, end2 = right;

	int i = left;
	while (begin1 <=end1 && begin2 <= end2)          
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[i++] = arr[begin1++];
		}
		else
		{
			tmp[i++] = arr[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}
	memcpy(arr + left, tmp + left, sizeof(int) * (right - left + 1));

	

}

void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(arr, 0, n-1,tmp);
	free(tmp);
}	
1. 时间复杂度:O(N*logN)
2. 空间复杂度:O(N)
3. 稳定性:稳定

 2.归并排序(非递归)

思路:
递归的思想是从最小区间开始排序,我们非递归也是一样,设置一个gap(间隔),先gap为1的排序,再是gap为2.... ,每次为gap*=2,直到gap达到数据总数为止。

void MergeSortNorR(int* arr, int n)
{
	int*tmp=(int*)malloc(sizeof(int) * n);
	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 (end1 >= n)
			{
				end1 = n-1;
				begin2 = n;
				end2 = n - 1;

			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

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

		}
		memcpy(arr, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);

}

注:

begin2,end1,end2注意越界问题!越界需修正。

五、计数排序

优化:

arr=[107,106,108,101,109,105,104];

数组中最大的数是109,我们就要开109个空间的count数组吗?

但是我们只用了7个空间,足足浪费了102个空间,所以我们选择一个优化方式,开范围个空间,这里的范围是数组中的最大值减去最小值。

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

	//int* tmp = (int*)calloc(max, sizeof(int));   可能min不是0
	int range = max - min + 1;           // 开辟了范围的空间   
	int* tmp = (int*)calloc(range, sizeof(int));
	

	for (int i = 0; i < n; i++)
	{
		tmp[arr[i]-min]++;                    //减少开辟空间 and 可以处理负数
	}

	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (tmp[i]--)
		{
			//arr[j++] = i;             没有复原         
			arr[j++] = i+min;         //复原原数 
		}
	}

}
1. 时间复杂度:O(MAX(N,范围))
2. 空间复杂度:O(范围)
3. 稳定性:稳定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JJY_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值