数据结构排序算法

转载请注明出处: http://blog.csdn.net/anzelin_ruc/article/details/9294459

冒泡排序:

重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到不再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,故名。

算法步骤:

1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。对所有元素在一趟比较之后,最后的元素应该就是最大的数。

3.针对除最后已排好序的所有的元素重复以上步骤1和2,直到没有任何一对数字需要比较。

时间复杂度:

若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数和记录移动次数均达到最小值:C(min) = n-1, M(min) = 0。所以,冒泡排序最好的时间复杂度为:o(n)。

若初始文件是反序的,需要进行趟排序。每趟排序要进行次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:C(max) = n(n-1)/2 = o(n^2) M(max) =  3n(n-1)/2 = o(n^2)。所以,冒泡排序的最坏时间复杂度:o(n^2)

综上,因此冒泡排序总的平均时间复杂度为:o(n^2)。

空间复杂度:

该算法只有在进行数据交换时最多需要一个临时变量,因此空间复杂度为o(1)。

算法稳定性:

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,则不需要进行交换;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,因此相同元素的前后顺序不会发生改变,冒泡排序是一种稳定排序算法。


#include <stdio.h>

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

void BubbleSort(int arr[],int len)
{
	/*需要n-1趟排序*/
	for (int i = 0; i < len - 1; ++i)
	{
		for (int j = 0; j < len -1 - i; ++j)
		{
			if (arr[j] > arr[j+1])
			{
				swap(&arr[j],&arr[j+1]);
			}
		}
	}
}

int main(int argc, char const *argv[])
{
	int arr[10] = {9,8,7,6,5,4,3,2,1,0};
	int len = sizeof(arr)/sizeof(int);

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

	BubbleSort(arr,len);

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

直接插入排序:

有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序。

算法步骤:

1.从第一个元素开始,该元素可以认为已经被排序。
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
5. 将新元素插入到下一位置中。
6. 重复步骤2。

时间复杂度:

如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。

空间复杂度:

直接插入排序只有需要一个临时变量存储将要插入的数据,因此空间复杂度为o(1)。

算法稳定性:

直接插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

#include <stdio.h>

void InsertSort(int arr[],int n)
{
	for (int i = 1; i < n; ++i)
	{
		int tmp = arr[i];
		int j;
		for (j = i; j > 0 && arr[j-1] > tmp ; --j)
		{
			arr[j] = arr[j-1];
		}
		arr[j] = tmp;
	}
}

int main(int argc, char const *argv[])
{
	int arr[10] = {9,8,7,6,5,4,3,2,1,0};
	int len = sizeof(arr)/sizeof(int);
	for (int i = 0; i < len; ++i)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	
	InsertSort(arr,len);
	
	for (int i = 0; i < len; ++i)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

希尔排序:

希尔排序(Shell Sort)是插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。

算法步骤:

先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。

时间复杂度:

希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(N^(5/4)),但是现今仍然没有人能找出希尔排序的精确下界。

空间复杂度:

希尔排序只有需要一个临时变量存储将要插入的数据,因此空间复杂度为o(1)。

算法稳定性:

由于进行了多次直接插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。

#include <stdio.h>


void ShellSort1(int arr[],int n)
{
	/*步长为gap,每次排序后gap减半,知道gap =1 */
	for (int gap = n/2; gap > 0; gap /= 2)
	{
		/*对各组进行排序*/
		for (int i = gap; i < n; ++i)
		{
			int j;
			int tmp = arr[i];
			for (j = i; j >= gap && arr[j - gap] > tmp; j -= gap)
			{
				arr[j] = arr[j - gap];
			}
			arr[j] = tmp;
		}
	}


}


void ShellSort2(int arr[],int n)
{
	/*步长为gap,每次排序后gap减半,知道gap =1 */
	for (int gap = n/2; gap > 0; gap /= 2)
	{
		/*对各组进行排序*/
		for (int i = gap; i < n; ++i)
		{
			int j;
			int tmp = arr[i];
			for (j = i; j >= gap && arr[j -gap] > tmp; j -= gap)
			{
				arr[j] = arr[j - gap];
				arr[j -gap] = tmp ;
			}
			
		}
	}


}


int main(int argc, char const *argv[])
{
	int arr[10] = {9,8,7,6,5,4,3,2,1,0};
	int len = sizeof(arr)/sizeof(int);
	for (int i = 0; i < len; ++i)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");


	ShellSort1(arr,len);


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


	return 0;
}


简单选择排序:

每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。 

算法步骤:

n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:
1.初始状态:无序区为R[1..n],有序区为空。
2.第1趟排序在无序区R[1..n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换,使R[1..1]和R[2..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。

3.第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
这样,n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果。


时间复杂度:

选择排序的交换操作介于 0 和 (n - 1) 次之间。选择排序的比较操作为 n (n - 1) / 2 次之间。选择排序的赋值操作介于 0 和 3 (n - 1) 次之间。 比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。因此简单选择排序的时间复杂度是O(n^2)。

空间复杂度:

这里需要一个额外的空间存储当前临时的最小值,因此空间复杂度为O(1)。

算法稳定性:

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

#include <stdio.h>

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

void SimpleSelectSort(int arr[],int len)
{
	
	for (int i = 0; i < len; ++i)
	{
		int min_index = i;
		for (int j = i + 1; j < len ; ++j)
		{
			if (arr[j] < arr[min_index])
			{
				min_index = j;
			}
		}
		if (min_index != i)
		{
			swap(&arr[i],&arr[min_index]);
		}
	}
}


int main(int argc, char const *argv[])
{
	int arr[10] = {9,8,7,6,5,4,3,2,1,0};
	int len = sizeof(arr)/sizeof(int);

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

	SimpleSelectSort(arr,len);

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

堆排序:

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,可以利用数组的特点快速定位指定索引的元素

算法步骤:

堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。 (1)用大根堆排序的基本思想 :
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区 。
② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key 。
③由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。 …… 直到无序区只有一个元素为止。 
(2)大根堆排序算法的基本操作:
 ① 初始化操作:将R[1..n]构造为初始堆。
 ② 每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
 注意:
 ①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。 
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止

时间复杂度:

堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成。堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件

空间复杂度:

和上面算法一样,堆排序是就地排序,辅助空间为O(1)。

算法稳定性:

堆排序是不稳定的排序方法。
#include <stdio.h>
void swap(int *a,int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
/*堆调整*/
void HeapAdjust(int *arr,int i,int size){
	int lchild = 2*i+1;    //节点i的左子节点
	int rchild = 2*(i+1);  //节点i的右子节点
	int max = i;           
	if (i <= size/2)       //只对非叶节点进行调整
	{
		if (lchild < size && arr[lchild] > arr[max])
		{
			max = lchild;
		}
		if (rchild < size && arr[rchild] > arr[max])
		{
			max = rchild;
		}


		if (max != i)
		{
			swap(&arr[i],&arr[max]);
			HeapAdjust(arr,max,size);//对调整过的max节点重新进行堆调整
		}
	}


}
/*建立无序大顶堆*/
void BulidHeap(int *arr,int size)
{
	for (int i = size/2; i >= 0; --i)
	{
		HeapAdjust(arr,i,size);
	}
}


/*堆排序*/
void HeapSort(int *arr, int size)
{
	BulidHeap(arr,size);
	for (int i = size - 1; i > 0; --i)
	{
		swap(&arr[0], &arr[i]);
		HeapAdjust(arr,0,i);
	}
}


int main(int argc, char const *argv[])
{
	int size = 10;
	int arr[10] = {9,8,7,6,5,4,3,2,1,0};
	for (int i = 0; i < size; ++i)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
	HeapSort(arr, size);


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



归并排序:

归并(Merge)排法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列的排序算法。归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

算法步骤:

归并操作的工作原理如下:第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置重复步骤3直到某一指针达到序列尾将另一序列剩下的所有元素直接复制到合并序列尾。
例如:
如 设有数列{6,202,100,301,38,8,1} 
初始状态: [6] [202] [100] [301] [38] [8] [1]       比较次数 
i=1    [6 202 ] [ 100 301] [ 8 38] [ 1 ]3
i=2[ 6 100 202 301 ] [ 1 8 38 ]
i=3 [ 1 6 8 38 100 202 301 ]4
 总计: 11次

时间复杂度:

将参加排序的初始序列分成长度为1的子序列进行第一趟排序,得到 n / 2 个长度为 2 的各自有序的子序列(若n为奇数,还会存在一个最后元素的子序列),再调用排序函数进行第二趟排序,得到 n / 4 个长度为 4 的各自有序的子序列, 第 i 趟排序就是两两归并长度为 2^(i-1) 的子序列得到 n / (2^i) 长度为 2^i 的子序列,直到最后只剩一个长度为n的子序列。由此看出,一共需要 log2n 趟排序,每一趟排序的时间复杂度是 O(n), 由此可知 该算法的总的时间复杂度是是 O(n log2n)。

空间复杂度:

归并排序算法需要和原数据规模一样大的辅助空间 ,因此其空间复杂度是 O(n)。

算法稳定性:

归并(Merge)排序法是一个稳定的排序算法。

#include <stdio.h>

int arr[10] = {9,8,7,6,5,4,3,2,1,0};
int tmp[10]; 

void Merge(int begin,int middle,int end)
{
	int i = begin;
	int j = middle + 1;
	int k = begin;
	
	while(i <= middle && j <= end)
	{
		if (arr[i] <= arr[j] )
		{
			tmp[k++] = arr[i++];
		}
		else
		{
			tmp[k++] = arr[j++];
		}
	}

	while(i <= middle)
	{
		tmp[k++] = arr[i++];
	}

	while(j <= end)
	{
		tmp[k++] = arr[j++];
	}

	for (k = begin; k <= end; ++k)
	{
		arr[k] = tmp[k];
	}
}

void MergeSort(int begin,int end)
{
	if (begin < end)
	{
		int middle = (begin + end) / 2;
		MergeSort(begin,middle);
		MergeSort(middle + 1,end);
		Merge(begin,middle,end);
	}
}


int main(int argc, char const *argv[])
{
	int len = sizeof(arr)/sizeof(int);
	for (int i = 0; i < len; ++i)
	{
		printf("%d ",arr[i] );
	}
	printf("\n");
	
	MergeSort(0,len - 1);
	
	for (int i = 0; i < len; ++i)
	{
		printf("%d ",arr[i] );
	}
	printf("\n");
	return 0;
}


快速排序:

快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

算法步骤:

一趟快速排序的算法是:
 1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
 2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
 3)从j开始向前搜索,即由后开始向前搜索(j -- ),找到第一个小于key的值A[j],A[i]与A[j]交换; 
4)从i开始向后搜索,即由前开始向后搜索(i ++ ),找到第一个大于key的A[i],A[i]与A[j]交换; 
5)重复第3、4、5步,直到 I=J; (3,4步是在程序中没找到时候j=j-1,i=i+1,直至找到为止。找到并交换的时候i, j指针位置不变。另外当i=j这过程一定正好是i+或j-完成的最后令循环结束。
例如:
待排序的数组A的值分别是:(初始关键数据: key=49) 注意关键 key永远不变,永远是和 key进行比较,无论在什么位置,最后的目的就是把 key放在中间,小的放前面大的放后面。
A[0]
A[1]
A[2]
A[3]
A[4]
A[5]
A[6]
49
38
65
97
76
13
27
进行第一次交换后:27 38 65 97 76 13 49
( 按照算法的第三步从后面开始找,此时:J=6)
进行第二次交换后:27 38 49 97 76 13 65
( 按照算法的第四步从前面开始找> key的值,65>49,两者交换,此时:I=2 )
进行第三次交换后:27 38 13 97 76 49 65
( 按照算法的第五步将又一次执行算法的第三步从后开始找
进行第四次交换后:27 38 13 49 76 97 65
( 按照算法的第四步从前面开始找大于 key的值,97>49,两者交换,此时:I=3,J=5 )
此时再执行第三和四步的时候就发现i=J=4,从而结束一趟快速排序,那么经过一趟快速排序之后的结果是:27 38 13 49 76 97 65,即所有大于 key49的数全部在49的后面,所有小于 key(49)的数全部在 key(49)的前面。

时间复杂度:

快速排序每次将待排序数组分为两个部分,在理想状况下,每一次都将待排序数组划分成等长两个部分,则需要logn次划分。而在最坏情况下,即数组已经有序或大致有序的情况下,每次划分只能减少一个元素,快速排序将不幸退化为冒泡排序,所以快速排序时间复杂度下界为O(nlogn),最坏情况为O(n^2)。在实际应用中,快速排序的平均时间复杂度为O(nlogn)

空间复杂度:

快速排序需要一个额外的空间存储关键元素(pivot element),因此空间复杂度为O(1)。

算法稳定性:

快速排序不稳定,且是在关键元素和某个元素发生交换时导致稳定性被破坏,比如序列5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会破坏元素3的稳定性。

#include <stdio.h>
#include <string.h>
#include <malloc.h>

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

int partition(int *arr,int low,int high)
{
	int pivot = arr[low];
	while(low < high)
	{
		while(low < high && arr[high] >= pivot)
		{
			high--;
		}
		if (low < high)
		{
			swap(&arr[low],&arr[high]);
			low++;
		}
		while(low < high && arr[low] <= pivot)
		{
			low++;
		}
		if(low < high)
		{
			swap(&arr[low],&arr[high]);
			high--;
		}
	}

	return low;

}
void quickSort(int *arr,int low,int high)
{
	int pivotpos;
	if (low < high)
	{
		pivotpos = partition(arr,low,high);
		quickSort(arr,low,pivotpos-1);
		quickSort(arr,pivotpos+1,high);
	}
}


int main(int argc, char const *argv[])
{
	int n ;
	printf("please input the length of arr:\n");
	scanf("%d",&n);
	//int *arr  = (int*)malloc(n * sizeof(int));
	int arr[n];
	printf("please input  %d numbers for each elements\n", n);
	for(int i = 0; i < n; i++)
	{
		scanf("%d",&arr[i]);
		
	}
	quickSort(arr,0,n-1);
	for (int i = 0; i < n; ++i)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
	return 0;
}

以上就是基本的数据结构排序算法,欢迎补充和讨论。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值