数据结构之——排序

#include<stdio.h>
#include<stdlib.h>
#include<algorithm>  


using namespace std;

/*
DataStruct ——Sort
				   直接插入排序 1
		插入排序:  折半插入排序 1
				   希尔排序 1
		
				   冒泡排序 1
 		交换排序:  快速排序 1
		 
		 		   简单选择排序 1 
排序:  选择排序:  堆排序 1
 
		归并排序 1
		基数排序 1(无代码) 

—————————— 
*/ 

/*
直接插入排序:
算法思想:一个待排序的序列[1...n]从第nums[2]开始,进行n-1趟操作,假定第i趟时,前i-1个元素[1...i-1]已经有序, 对于第i个元素,
从[1...i-1]中找到某一个位置j,使得nums[j-1] < nums[i] < nums[j],此时[j...i-1]均向后移一位成为[j+1...i],腾出位置让nums[i]插入,
[1...i]变得有序。 

时间复杂度;
			最好:O(n) 整个序列正序 
			最坏:O(n2) 整个序列是逆序的, 
			平均: O(n2) 
			
空间复杂度:O(1) (用于记录需要插入的数据)

稳定性:稳定 (先比较后移动) 
*/

void DirectInsert(int nums[], int len){
	for(int i = 1;i < len;i++){//从序列中的第二个元素开始(数组中的元素下表从0开始,所以nums[1]为第二个元素) 
		int temp = nums[i], j = i; //temp记录nums[i] 
		while(j > 0 && nums[j - 1] > temp){ //从后往前寻找位置j 要寻找的是小于temp的第一个元素 
			nums[j] = nums[j - 1]; //寻找的过程中将不符合条件的元素(所有大于temp的元素)后移一位 
			j--; //每次向前一位寻找 
		}
		nums[j] = temp; //最后找到了j的位置,并且已经将j空出来了,直接进行插入即可 
	} 
	printf("DirectInsert ");
	for(int i = 0;i < len;i++){
		printf("%d ", nums[i]);
	}
	printf("\n");
} 

/*
折半插入排序:

算法思想:先通过折半查找找出待排序的元素在有序序列中应该插入的位置,然后统一把元素向后移,直接插入排序的升级 

时间复杂度:比起直接插入排序,减少了比较元素的次数 为O(nlogn),移动次数并没有改变,故时间复杂度与直接插入排序相同 
			最好:O(n)(与简单插入排序同) 
			最坏:O(n2)
			平均:O(n2)

空间复杂度:O(1) 

稳定性:稳定 
 
*/ 

void BinarySort(int nums[], int len){
	int i, j, low, high, mid, temp;
	printf("BinarySort ");
	for(i = 1;i < len;i++){ //假设第0个已经排好序了,所以从1开始 
		temp = nums[i]; //暂存要排序的数
		low = 0, high = i - 1; //折半查找的范围是已经排好序的序列 
		while(low <= high){  //折半查找
			mid = (low + high) / 2;
			if(nums[mid] > temp){  
				high = mid - 1;
			}else{
				low = mid + 1;
			}
		} //查找完成后如果找到了,找到的要插入位置就是high + 1 
		for(j = i - 1;j >= high + 1;--j){ //需要后移的元素是 
			nums[j + 1] = nums[j]; // 待排序的元素的前一个(i - 1)位置到该插入的位置 
		}
		nums[high + 1] = temp; //把待排序元素插入 
	}
	
	for(int i = 0;i < len;i++){
		printf("%d ", nums[i]);
	}
	printf("\n");
} 

/*
希尔排序(缩小增量排序):
先将待排序序列分割成若干形如[i, i+d, i+2d, ..., i+kd]的子表,分别进行直接插入排序或者折半插入排序,
算法思想:先取一个小于n的步长d1(每d1个取一个元素),把表中的所有元素分成d1组,所有距离为d1的倍数的元素放在一组,在各组中进行直接插入排序或者折半插入排序 
		  然后取第二个步长d2 < d1,重复上述过程,直到所取的di = 1,即所有的元素已经放在一组,

时间复杂度:希尔排序的时间复杂度和其增量序列有关系,这涉及到数学上尚未解决的难题;不过在某些序列中复杂度可以为O(n1.3);
			最坏情况下为O(n2) 
			
空间复杂度:O(1) 

稳定性:不稳定 
		当相同关键字的元素被分到两组的时候,它们的相对顺序可能会发生改变。
		如[3, 2*, 2] 取增量为2,经过一次排序后,序列变为[2, 2*, 3],最终的排序序列也是[2, 2*, 3],显然,2,2*的相对顺序已经发生了改变 

*/

void ShellSort(int nums[], int len){
	for(int d = len / 2;d >= 1;d /= 2){ // 循环选取增量 
		for(int i = d;i < len;i++){ // 取所有组的第一个元素作为基点,从这以后循环的对每一组的元素进行操作 
			if(nums[i] < nums[i - d]){ //如果小于本组的上一个元素,则需要进行插入(这里只需要与同组的上一个元素比较即可,因为它就是本组中之前所有元素中最大的元素(之前的元素已经有序),只要小于它,就一定会有插入的位置) 
				//该组后移的方法是:先存下nums[i],将大于nums[i]的元素直接覆盖nums[i], 
				int temp = nums[i]; //暂存数据
				int j = i - d;
				while(j >= 0 && temp < nums[j]){  //寻找j的位置顺便把大于temp的值后移 
					nums[j + d] = nums[j]; 
					j -= d;
				} 
				//while 和 for两种写法都可 
			//	for(j = i - d;j >= 0 && nums[j] > temp;j -= d){
			//		nums[j + d] = nums[j]; 
			//	} 
				
				nums[j + d] = temp; //这里为什么是 j+d 呢?因为j最后是-d后退出循环的,真正应该插入的位置是 j+d 
			}
		}
	} 
	printf("ShellSort ");
	for(int i = 0;i < len;i++){
		printf("%d ", nums[i]);
	} 
	printf("\n");
} 


/*
冒泡排序:

算法思想:对于一个待排序序列,从前往后(或者从后往前),两两比较相邻元素的值,如果是逆序(nums[i - 1] > nums[i]),则交换它们,直至遍历完整个序列。
		  这一趟比较的结果是,未排序序列中最小的元素放在了未排序序列的第一个。一趟排序称为一趟冒泡,下一趟冒泡时,前一趟所确定的最小元素不再参与排序,
		  待排序元素减少一个。每一趟冒泡都能确定一个元素的最终位置。 
		  经过n-1趟排序,所有元素都已有序。 

时间复杂度:最好:整个序列正序排列,O(n) 比较 n-1 次, 移动 0 次  
			最坏:整个序列逆序排列,O(n2) 比较次数 (n2 - n) / 2, 移动次数: (3n2 - 3n) / 2 
			平均: O(n2)

空间复杂度:O(1)

稳定性:稳定     因为是两两之间有绝对大小关系时才会移动,故其是稳定的。 


*/

void BubbleSort(int nums[], int len){
	printf("BubbleSort ");
	for(int j = 0;j < len - 1;j++) // n-1 趟排序 
	for(int i = len - 1;i > j;i--){ // 从后往前进行比较 
		if(nums[i] < nums[i - 1]){
			std::swap(nums[i], nums[i - 1]); //如果找到比某个元素小于它的前一个元素的话,进行交换,使得较小的元素在前,此时nums[i-1]是[i-1...n]的元素中最小的值 
		}
	}	
	
	for(int i = 0;i < len;i++){
		printf("%d ", nums[i]);
	}
	printf("\n");
} 

/*

快速排序:对于冒泡排序的改进 

算法思想:在待排序列表[1...n]中,取某一个元素pivot作为基准,通过一趟排序将待排序列表划分为独立的两部分L[1...k-1],L[k+1...n],
		  使得L[1...k-1]中的所有元素都小于pivot,L[k+1...n]中的所有元素都大于pivot,则pivot放在了它在整个列表中的最终位置L[k],这就是一趟快速排序。
		  然后递归的对L[1...k-1] 和 L[k+1..n]重复上述过程,直至每部分内都只有一个元素或者空为止,此时所有元素都放在了其最终位置。 

时间复杂度:
			快速排序的时间复杂度 = 递归树的深度*每层比较的次数
			快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在数组已经有序的情况下,每次划分将得到最坏的结果。
			一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。
			实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。
			一位前辈做出了一个精辟的总结:“随机化快速排序可以满足一个人一辈子的人品需求。”
  		随机化快速排序的唯一缺点在于,一旦输入数据中有很多的相同数据,随机化的效果将直接减弱。
			对于极限情况,即对于n个相同的数排序,随机化快速排序的时间复杂度将毫无疑问的降低到O(n^2)。解决方法是用一种方法进行扫描,使没有交换的情况下主元保留在原位置。
			
			最好:每次都划分后左右子序列的大小都相等,O(nlogn) 
			最坏:元素已经有序时 O(n2) 划分过程产生的两个区域分别包含n-1个元素和1个0元素的时候 O(n2) 
			平均: O(nlogn) 

空间复杂度: 由于快速排序是递归的,递归需要借助栈来时间,
			 最好情况下,partition每次均匀划分,如果排序n个关键字,递归树深度为.log2n. + 1(.x.表示对x向下取整),即仅需递归log2n次。
			 最坏情况下,待排序的序列为正序或者逆序(即有序),每次划分得到的序列只会减少一个记录,另一个为空序列。需进行 n-1 次递归,因此栈的深度为O(n)。
			 平均情况下,栈的深度为O(logn) 

稳定性: 不稳定,相同的两个元素如果被分到两组的话 

*/

int Partition(int nums[], int low, int high){
	int pivot = nums[low]; //将表的首元素设为枢纽值,此时nums[low]腾出位置 
	//将所有比pivot小的元素放到表的左边,将所有比pivot大的元素放到表的右边 
	while(low < high){
		//在右半边,从高往低找到第一个比pivot小的元素 
		while(low < high && nums[high] >= pivot){
			high--;
		}
		nums[low] = nums[high]; //找完后进行交换,因为之前已经进行暂存数据,所以这里直接覆盖,此时nums[high]空出位置 
		//在左半边,从低往高找到第一个比pivot大的元素 
		while(low < high && nums[low] <= pivot){
			low++;
		}
		nums[high] = nums[low];
	}//跳出循环的条件是low >= high 
	nums[low] = pivot; //枢纽元素最终存放的位置 
	
	return low; //返回枢纽元素的最终位置 
	
}

void QuickSort(int nums[], int low, int high){
	if(low <  high){
		int pivot = Partition(nums, low, high); //获取枢纽位置 
		QuickSort(nums, low, pivot - 1); //排序左边 
		QuickSort(nums, pivot + 1, high); //排序右边 
	}
	
}



/*
简单选择排序:
算法思想:每次从未排序序列[i...n]中选择最小的放在未排序序列的第一个,这样,第i个元素与前面的[1...i-1]个元素成有序序列, 
          每趟排序都可以确定一个元素的最终位置。 共需n-1次排序 

时间复杂度:最好:O(n)  序列中所有元素正序排序 
			最坏:O(n2) 序列中所有元素逆序排序 
			平均:O(n2) 
空间复杂度:O(1)  简单选择排序需要占用 1 个临时空间,在交换数值时使用。

稳定性:不稳定 
		取序列为[5, 5*, 2] 由于每次要将未排序的序列中最小值与未排序序列第一个值交换,第一次交换后变为[2, 5*, 5],
		排序完成后为[2, 5*, 5],可见,简单选择排序是一种不稳定的排序 

*/
 
void simpleSelect(int nums[], int len){
	//从数组开头开始排序,这里写nums.length - 1也可以,前n-1个元素都已经排好序了,最后剩下的一个元素一定是整个序列中最大的元素。 
	for(int i = 0;i < len - 1;i++){
		int k = i;  
		for(int j = i + 1;j < len;j++){ //从i + 1->n寻找最小的元素 
			if(nums[j] < nums[k]){
				k = j; 
				
				
				 //记录下最小元素的下表 
			}
		}
		//搜索到最小元素后进行交换 
		int temp = nums[k]; // 此时nums[k]为数组中最小的元素,[i...n]仍是无序的。 
		nums[k] = nums[i];	//将其与nums[i]交换,此时[0...i]有序,[i+1...n]仍然无序。
		nums[i] = temp;	
	}
	printf("simpleSelect ");
	for(int i = 0;i < len;i++){
		printf("%d ", nums[i]);
	}
	printf("\n");
}



/*
堆排序:
大根堆:每个节点的值都大于其左右孩子。 根节点为为最大元素 

小根堆:每个结点的值都小于其左右孩子。 根节点为为最小元素 
堆的存储: 
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。

构造堆、调整堆的过程: 
					从 n/2向下取整个节点开始,因为最后一个节点一定是该节点的孩子,
					这是由二叉堆的定义所决定的(由于其它几种堆(二项式堆,斐波纳契堆等)用的较少,一般将二叉堆就简称为堆)。
					对第 n/2 向下取整 个节点进行筛选,使得其孩子节点满足堆的要求(最小堆使孩子节点均大于该节点,最大堆使孩子节点均小于该节点)
					之后依次对各节点(n/2 向下取整 - 1 ~ 1)进行筛选, 在筛选的过程中,有可能破坏下一级的堆, 按照上述方法进行调整,
					反复进行,直到根节点。如此便构成了一个堆。 

堆的删除和插入: 
		  插入:每次插入将新元素放到堆的末端,然后重新进行调整(对该节点进行向上调整)
		  删除:每次删除只能删除堆顶元素,将堆的最后一个元素直接赋值给堆顶元素,此时堆的结构如果遭到破坏,对此时的根节点进行向下调整。 
 
算法思想:构造好一个堆后(大根堆或小根堆, 以大根堆为例),堆顶元素即为整个序列中最大值。将其输出后(此时一个元素确定好其最终位置), 
		  通常将堆底元素送入堆顶(删除堆顶元素),此时整个堆的结构被破坏,不满足大根堆的定义,于是对其进行向下调整使整个堆仍然保持大根堆的性质,
		  调整好后,堆顶元素又为整个堆中的最大值,再次将其输出,重新调整,如此反复,直至所有元素排好序。
		   

时间复杂度:建堆时间为O(n),每次调整堆的时间复杂度为O(log n),总共有 n-1 次调整操作,故时间复杂度为O(n*logn) 

			
空间复杂度:O(1),只有需要常数个辅助空间。 

稳定性;不稳定。在一个长为n的序列中,堆排序的过程是从第 n/2 个元素和其孩子节点共三个值中进行筛选,这三个元素之间的选择自然不会破坏稳定性。
		但是,当 n/2 - 1, n/2 - 2 进行筛选的过程中,就可能破坏稳定性,有可能 n/2 个父节点把后面一个元素交换过去了,而
 		比如小根堆:3 27 36 27*,
		如果堆顶3先输出,27*跑到堆顶,然后堆稳定,继续输出堆顶,是27*,这样说明27*先于27输出,不稳定。
*/
void AdjustDown(int nums[], int k, int len){
	nums[0] = nums[k]; // nums[0]暂存数据 

	for(int i = 2 * k;i <= len;i *= 2){ //选取其子节点中值较大的的节点, 2*k 是其左孩子节点, 2*k+1 是其右孩子节点 
		if(i < len && nums[i] < nums[i + 1]){ 
			i++;// 如果右孩子节点的值更大,选取右孩子节点。 
		}
		if(nums[0] >= nums[i]){ //当前元素的值并不小于其左右孩子中任何一个节点的值,直接结束本次调整(不需要调整) 
			break;
		}
		else{ //将孩子节点的值(较大的)赋值给父节点(当前节点) 
			nums[k] = nums[i];
			k = i; //本次修改可能引起以该节点为根的堆的变化,修改k,以做出对应的调整 
		}
	}
	nums[k] = nums[0]; //把最开始的根节点的值赋给新的k,这个k是最终筛选的位置  如果父节点的值与其孩子节点进行交换的话,k就是孩子节点,如果没有交换的话,k不变 
}

void AdjustUp(int nums[], int k){
	nums[0] = nums[k];
	int i = k / 2;
	while(i > 0 && nums[i] < nums[0]){
		nums[k] = nums[i];
		k = i;
		i = k / 2;
	}
	nums[k] = nums[0];
}

void BuildMaxHeap(int nums[], int len){
	for(int i = len / 2;i > 0;i--){
		AdjustDown(nums, i, len);// 从len / 2 开始,因为这是整个序列中最后一个有孩子结点的结点,从后往前(一直到第一个结点),反复调整,构成大根堆 
	}
	
}

void HeapSort(int nums[], int len){
	BuildMaxHeap(nums, len); //初始化堆,堆顶是nums[1] 
	printf("%d ", nums[1]); 
	for(int i = len;i > 1;i--){ //堆中共有len个元素,len-1个元素进行排序,剩下一个自然有序 
		std::swap(nums[i], nums[1]); //输出堆顶元素,把堆底元素放到堆顶,进行新的调整 
		AdjustDown(nums, 1, i - 1); //把剩余的 i-1 个元素重新整理成堆(去除刚放到堆底的元素) 
		printf("%d ", nums[1]); 
	}
	
}


/*
归并排序:

算法思想:将待排序序列的n个元素视为有序的n个子表,将其两两合并,得到 n/2 个长度为2或者1的子表(合并的过程中进行排序),
		  再两两合并……如此重复,直到合并为一个长度为n的有序表为止。 

时间复杂度:最好:O(nlogn)   每趟归并的时间复杂度为O(n),共需进行logn次排序,时间复杂度为O(nlogn) 
			最坏:O(nlogn)
			平均:O(nlogn)

空间复杂度:需要一个额外的辅助数组B,空间复杂度为O(n) 

稳定性:稳定, 二路归并排序 将前后两个有序表合并为一个有序表的时候,不会改变相同关键字的相对顺序。 
*/

//nums[low...mid]和nums[mid+1, high]分别是nums中需要合并的有序的两个子表 

int length;

void Merge(int nums[], int low, int mid, int high){
	
	int B[length]; //辅助数组 
//	printf("low = %d mid = %d high = %d\n", low, mid, high);
//	printf("合并前: ");
//	for(int i = 0;i < length;i++){
//		printf("%d ", nums[i]);
//	}
//	printf("\n");
	
	for(int k = 0;k < length;k++){
		B[k] = nums[k]; //将nums 放到辅助数组中,把nums腾出来 
	}
	int i, j, k; //左指针和右指针
	for(i = low, j = mid + 1, k = i; i <= mid && j <= high;k++){
		if(B[i] <= B[j]){ // 比较B中左子表和右子表的值 
			nums[k] = B[i++]; //将较小的放到nums中 
		}
		else{
			nums[k] = B[j++];
		}
	}
	
	while(i <= mid){
		nums[k++] = B[i++]; //把左边剩余的元素放入数组 
		 
	}
	while(j <= high){
		nums[k++] = B[j++]; //把右边剩余的元素放入数组 
	}
	
	printf("合并后: ");
	for(int i = 0;i < length;i++){
		printf("%d ", nums[i]);
	}
	printf("\n");
}

void MergeSort(int nums[], int low, int high){
	if(low < high){
		int mid = (low + high) / 2;
		MergeSort(nums, low, mid); //递归处理左边 
		MergeSort(nums, mid + 1, high); //递归右边 
		Merge(nums, low, mid, high); //两边合并 
	} 
} 
 

/*
基数排序:
基于关键字各位的大小进行排序,借助 分配 和 收集 两种操作对单逻辑关键字进行排序。基数排序又分为最高位优先(MSD)排序和最低位优先(LSD)排序 
 
算法思想:(最低位优先) 
			待排序序列[0...n-1],每个元素由d元组[Kd-1...K1K0]组成,以r为基数,排序的过程中,使用r个队列,Q0,Q1,...,Qr-1。
			
			d为序列中元素的最大位数。r为元素的基(进制) 
			
			分配:开始时,把各个队列置为空,依次考察序列中的每个元素,从第0位(K0)开始,若其某位上为k,将其放入Qk队列 
			
			收集:将各个队列 Q0,Q1,...,Qr-1中的元素首尾相连,得到新的序列。
			继续对新序列分配K1位,不断重复。 
			进行d次排序,整个序列有序。 

时间复杂度:基数排序共需进行d次排序(分配与回收),每次分配需要O(n), 每次收集需要O(r),时间复杂度为O(d(n+r)) ,与序列的初始状态无关。 

空间复杂度:每次分配回收需要r个队列,可重复利用。 

稳定性:稳定 

*/


int main(){
	int nums[] = {0, 2, 7, 5, 4, 9, 10};
	int nums1[] = {5, 4, 7, 12, 4, 9, 2, 1, 13, 2}; //sort result 1 2 2 4 4 5 7 9 12 13
	int len1 = 10;
	int len = 7;
//	DirectInsert(nums1, len1);//直接插入排序 
//	simpleSelect(nums1, len1); //简单选择排序 
//	BinarySort(nums1, len1); // 折半插入 
//	ShellSort(nums1, len1); // 希尔排序 
//	BubbleSort(nums1, len1); //冒泡排序 
//	QuickSort(nums1, 0, len1 - 1); //快速排序 
//	printf("QuickSort ");
//	for(int i = 0;i < len1;i++){
//		printf("%d ", nums1[i]);	
//	}
//	printf("\n");
	
//	HeapSort(nums1, len1); //堆排序 
	length = len1;
	MergeSort(nums1, 0, len1 - 1); //归并排序 
	
	return 0;
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值