数据结构与算法之冒泡排序法,选择排序法,归并排序法,快速排序法比较.

191 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

对于c语言初学者来说第一个接触的排序方法一般是冒泡排序法.

冒泡排序法是通过设置双层循环嵌套来比较相邻的两个元素.

	for(j=1; j<=n; j++)//冒泡法排序.
					for(i=1; i<=n-j; i++) {
						if(a[i]>a[i+1]) {
							t=a[i];
							a[i]=a[i+1];
							a[i+1]=t;
						}
					}

而同样是通过设置双层循环嵌套的选择排序法则是将数组分为两段,一段为已排序列,另一端为未排序列,每次循环将未排序列中的第一个元素与剩下的所有元素比较,然后将目标元素放入已排序列中,直至整个数组为已排序列.

for(i=1; i<n; i++) {//选择排序法,和冒泡法不同的是,冒泡法是相邻的两数比较
					int k=i;//而选择排序法是未排序区域的第一个数与剩下的所有数比较.
					for(j=i+1; j<=n; j++) {
						if(a[j]<a[k])
							k=j;
						t=a[k];
						a[k]=a[i];
						a[i]=t;
					}
				}

image.png (该图片引自百度百科.)

冒泡排序法与选择排序法相比较.

一.冒泡排序法.

优点:比较简单,空间复杂度较低,是稳定的;

缺点:时间复杂度太高,效率慢.

二.选择排序法.

优点:一轮比较只需要换一次位置;

缺点:效率慢,不稳定,排序过程中原序列会遭到破坏.(举个例子5,8,5,2,9 我们知道第一遍选择第一个元素5会和2交换,那么原序列中2个5的相对位置前后顺序就破坏了)

归并排序法.

void mergesort(int *num, int start, int end) {//归并排序
	int middle;//利用递归,先分再治.
	if(start<end) {//当未拆分为单个的时候利用递归继续拆分.
		middle= (start+end)/2;
		mergesort (num, start, middle) ;
		mergesort (num, middle+1, end) ;
		merge (num, start, middle, end);//治--合并排序
	}
}
void merge(int *num, int start, int middle, int end) {
	int n1=middle-start+1;
	int n2=end-middle;
	/*	int *L=new int[n1+1];
		int *R=new int[n2+1];*/
	int L[n1+1] ;//动态分配内存,声明两个数组容纳左右两边的数组
	int R[n2+1] ;
	int i, j=0, k;
	for (i=0; i<n1; i++) {//将新建的两个数组赋值
		*(L+i)=* (num+start+i) ;
	}
	*(L+n1)=1000000;
	for (i=0; i<n2; i++)
		* (R+i)=* (num+middle+i+1);
	* (R+n2) =1000000;
	i=0;
	for (k=start; k<=end; k++) {//进行合并.
		if(L[i]<=R[j]) {
			num[k]=L[i];
			i++;
		} else {
			num[k]=R[j];
			j++;
		}
	}
}

具体思想如下图:

image.png

image.png (图片来自:图解排序算法(四)之归并排序 - dreamcatcher-cx - 博客园 (cnblogs.com),侵权联删.)

归并排序法就是利用递归思想将原数组一步一步拆分为更小的数组,然后对其合并排序.

归并排序是种稳定排序,能利用完全二叉树特性进行排序是一种高效的排序方法,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。

快速排序法

#include <stdio.h>

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

int partition(int arr[], int low, int high) {
    int pivot = arr[high];
    int i = (low - 1);

    for (int j = low; j <= high - 1; j++) {
        if (arr[j] < pivot) {
            i++;
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i + 1], &arr[high]);
    return (i + 1);
}

void quick_sort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);

        quick_sort(arr, low, pi - 1);
        quick_sort(arr, pi + 1, high);
    }
}

int main() {
    int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    quick_sort(arr, 0, n - 1);

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

快速排序法的基本思想是通过选择一个基准元素,将数组分为两部分,使得左边的元素都小于等于基准,右边的元素都大于等于基准,然后递归地对左右两部分进行排序。

快速排序的排序基准的选择是一个关键因素,它会影响算法的性能。通常情况下,排序基准的选择是有策略的,而不是随意选取的。

一般而言,排序基准的选择有以下几种常见策略:

  1. 固定位置基准: 选择数组的第一个元素、最后一个元素或中间元素作为基准。这种方法的实现较简单,但在某些特定情况下可能会导致快速排序的性能下降,例如已经接近有序的数组。
  1. 随机基准: 在数组中随机选择一个元素作为基准。这可以在某种程度上避免在特定情况下出现性能问题,但可能需要更多的比较操作。
  1. 三数取中基准: 从数组的第一个、中间和最后一个元素中选择中间大小的元素作为基准。这可以在一定程度上避免最坏情况的发生,提高算法的性能。
  1. 随机化选择: 在数组中随机选择多个元素,然后从这些元素中选取中位数作为基准。这种方法可以进一步提高快速排序的性能。

选择不同的基准策略会影响快速排序的性能,使其在不同情况下表现更好。在实际应用中,根据数据的特点选择适当的基准策略可以提高算法的效率。

归并排序法和快速排序法比较:

需要注意的是,快速排序法在平均情况下具有较好的性能,但在最坏情况下可能会退化为O(n^2)的时间复杂度。归并排序法则在所有情况下都保持稳定的O(n log n)时间复杂度,但需要额外的空间来存储中间结果。在实际应用中,根据情况选择适合的排序算法可以帮助提高性能。

三种排序方法实际时间比较测试.

生成5000个随机整数以便于三种排序方法的测试.

一.冒泡排序法.

image.png

image.png 二.选择排序法.

image.png

image.png 三.归并排序法.

image.png

image.png 小结:经过多次尝试可得出结论相较于冒泡排序法和选择排序法来说,归并排序法能够明显减少排序时间,并且就稳定性来说归并排序法也是优于冒泡排序法的.

(另附比较三种排序方法效率的程序的代码.)

#include <stdlib.h>
#include <iostream>
#include <time.h>
using namespace std;
void mergesort(int *num, int start, int end) ;//函数的声明.
void merge(int *num, int start, int middle, int end) ;
//如果不先声明的话,当调用该
//函数时,编译器发现一个不认识的函数调用,不知道该函数的返回类型,就假设为int类型,
//等后面编译的时候编译器看到实际的函数,它认为有两个同名的函数,一个是文件中的函数
//,一个是编译器假设返回int的那个。为了防止编译器假设函数的返回类型,
//你可以显式地告诉它。告诉编译器函数会返回什么类型的语句就叫函数声明。
void mergesort(int *num, int start, int end) {//归并排序
	int middle;//利用递归,先分再治.
	if(start<end) {//当未拆分为单个的时候利用递归继续拆分.
		middle= (start+end)/2;
		mergesort (num, start, middle) ;
		mergesort (num, middle+1, end) ;
		merge (num, start, middle, end);//治--合并排序
	}
}
void merge(int *num, int start, int middle, int end) {
	int n1=middle-start+1;
	int n2=end-middle;
	/*	int *L=new int[n1+1];
		int *R=new int[n2+1];*/
	int L[n1+1] ;//动态分配内存,声明两个数组容纳左右两边的数组
	int R[n2+1] ;
	int i, j=0, k;
	for (i=0; i<n1; i++) {//将新建的两个数组赋值
		*(L+i)=* (num+start+i) ;
	}
	*(L+n1)=1000000;
	for (i=0; i<n2; i++)
		* (R+i)=* (num+middle+i+1);
	* (R+n2) =1000000;
	i=0;
	for (k=start; k<=end; k++) {//进行合并.
		if(L[i]<=R[j]) {
			num[k]=L[i];
			i++;
		} else {
			num[k]=R[j];
			j++;
		}
	}
}
int main() {
	while(1) {
		clock_t start,finish;
		double duration;
		int n,i,j,t,b;
		float tim;
		int a[100000];
		cout<<"输入个数(100000以内)"<<endl;
		cin>>n;
		srand (time(0));
		for(i=1; i<=n; i++) {
			a[i]=rand();
		}
		cout<<"产生的随机数:"<<endl;
		for(j=1; j<=n; j++) {
			cout<<a[j]<<"\t";
		}
		cout<<endl;
		/*	for(j=1; j<=n; j++)
				cout<<a[j]<<"\t";
			cout<<endl;*/
 
		cout<<" ****选择****" <<endl;
		cout<<" (1)冒泡排序"<<endl;
		cout<<" (2)选择排序"<<endl;
		cout<<" (3)归并排序"<<endl ;
		cout<<" (4)退出"<<endl;
		cin>>b;
		if(b==4)
			break;
		switch(b) {
			case 1: {
				start = clock() ;
				for(j=1; j<=n; j++)//冒泡法排序.
					for(i=1; i<=n-j; i++) {
						if(a[i]>a[i+1]) {
							t=a[i];
							a[i]=a[i+1];
							a[i+1]=t;
						}
					}
				cout<<"冒泡排序结果"<<endl;
				for(j=1; j<=n; j++)
					cout<<a[j]<<"\t";
				cout<<endl;
				finish = clock();
				tim= (double)(finish - start) / CLOCKS_PER_SEC;
				cout<<"用时:"<<endl<<tim<<endl;
			}
			break;
			case 2: {
				start = clock() ;
				for(i=1; i<n; i++) {//选择排序法,和冒泡法不同的是,冒泡法是相邻的两数比较
					int k=i;//而选择排序法是未排序区域的第一个数与剩下的所有数比较.
					for(j=i+1; j<=n; j++) {
						if(a[j]<a[k])
							k=j;
						t=a[k];
						a[k]=a[i];
						a[i]=t;
					}
				}
				cout<<"选择排序结果"<<endl;
				for(j=1; j<=n; j++)
					cout<<a[j]<<"\t";
				cout<<endl;
				finish = clock() ;
				tim = (double) (finish - start) / CLOCKS_PER_SEC;
				cout<<"用时:"<<endl<<tim<<endl;
			}
			break;
			case 3: {
				start = clock() ;
				mergesort (a,0,n) ;//利用递归处理
				cout<<"归并排序结果"<<endl;
				for (i=1; i<=n; i++)
					cout<<a[i]<<"\t";
				cout<<endl;
				finish = clock() ;
				tim = (double) (finish-start)/CLOCKS_PER_SEC;
				cout<<"用时:"<<endl<<tim<<endl;
			}
			break;
		}
	}
	return 0;
}