[排序]选择排序、冒泡排序、插入排序、希尔排序、归并排序、快速排序、堆排序算法及比较

[排序]选择排序、冒泡排序、插入排序、希尔排序、归并排序、快速排序、堆排序算法及比较

1.选择排序

    从数组中选择最小的元素,将它与数组的第一个元素交换位置,再讲数组剩下的元素中选择最小的元素,将它与数组的第二个元素交换位置,重复操作,直到将整个数组排序。

选择排序:

选择排序
    选择排序需要N2/2次比较和N次交换,对已经排序的数组也需要这么多次比较和交换操作。

public void Sort(int[] nums) {
	int len=nums.length();
	for(int i=0;i<len-1;i++){
		int min=nums[i];
		for(int j=i+1;j<len;j++){
			if(nums[j]<min)
				min=nums[j];
		}
		swap(nums[i],min);
	}
}

时间复杂度:最好O(n2) 最坏O(n2)
空间复杂度:O(1)
稳定性:不稳定

2.冒泡排序

    数组中相邻的元素进行比较,如果顺序就不交换,如果顺序错误就交换,每次让未排序的最小元素浮到左侧,或者最大元素移动右侧。

第一次排序:

第一次排序

第二次排序:

第二次排序

第三次排序:

第三次排序

第四次排序:

第四次排序

2.1常规版
public void Sort(int[] nums) {
	int len=nums.length();
	for(int i=0;i<len;i++){
		//最小元素移到左侧
		for(int j=len-1;j>i;j--){
			if(nums[j-1]>nums[j])
				swap(nums[j-1],nums[j]);
		}
		
		//如果最大元素移到右侧
		/*		
		for(int j=0;j<len-i-1;j++){
			if(nums[j]>nums[j+1])
				swap(nums[j-1],nums[j]);
		}
		*/
	}
}

时间复杂度:最好O(n2) 最坏O(n2)
空间复杂度:O(1)
稳定性:稳定

2.2第一次改进

考虑[2,1,3,4,5]进行冒泡排序
第一次排序:1,2,3,4,5
第二次排序:1,2,3,4,5
第三次排序:1,2,3,4,5
第四次排序:1,2,3,4,5
第一次循环就已经完成了排序,但是仍会继续后面的流程,显然是多余的。
    为了解决这个问题,可以设置一个标志位,用来表示是否有交换,如果有交换继续下一次循环,如果没有则停止。

public void Sort(int[] nums) {
	int len=nums.length();
	for(int i=0;i<len;i++){
		int flag=1;
		//最小元素移到左侧
		for(int j=len-1;j>i;j--){
			if(nums[j-1]>nums[j]){
				swap(nums[j-1],nums[j]);
				flag=0;
			}
		}
		if(flag==1)//如果没有交换过元素,说明已经有序
			return;
	}
}

    这一次优化之后,假如从小到大排序[1,2,3,4,5]有序数组,则只会进入一次循环,此时的时间复杂度为O(n)。
时间复杂度:最好O(n) 最坏O(n2)

2.3第二次改进

    考虑内循环长度,假如第i次排序时,最后一次产生交换的位置为index,则说明index之前的元素已经排好序了,那么第i+1次排序时,就可以直接从尾判断到index停止。
    设置一个index标志位,标记最后一次产生交换时的位置,缩小内循环。

public void Sort(int[] nums) {
	int len=nums.length();
	int temppos=0;
	int index=0;
	for(int i=0;i<len;i++){
		int flag=1;
		//最小元素移到左侧
		index=temppos;//判断到上一次排序时最后一次产生交换的位置
		for(int j=len-1;j>index;j--){
			if(nums[j-1]>nums[j]){
				swap(nums[j-1],nums[j]);
				flag=0;
				temppos=j;
			}
		}
		if(flag==1)//如果没有交换过元素,说明已经有序
			return;
	}
}

算法得到了进一步的优化,可以去掉内循环中多余的步骤。
由于至少需要循环进行一次比较,所以时间复杂度还是 最好O(n) 最坏O(n2)

3.插入排序

    直接插入排序将无序序列中的元素插入有序序列中,遍历无序序列,拿无序序列中的元素与有序序列中的元素进行比较,找到合适的位置然后插入。

插入排序:

插入排序

3.1常规版
public void Sort(int[] nums) {
	int len=nums.length();
	for(int i=0;i<len;i++){
		for(int j=i+1;j>=0;j--){
			if(nums[j]<num[j-1])
				swap(nums[j],nmus[j-1]);
		}
	}
}

时间复杂度主要取决于比较次数和交换次数
比较次数1+2+3+……+n ~= n2/2

时间复杂度:最好O(n2) 最坏O(n2)
空间复杂度:O(1)
稳定性:稳定

3.2改进

    考虑有序数组[1,2,3,4,5]的最后一次循环,5与前面已经排好序的[1,2,3,4]比较,5>4那么就可以停止内循环不再与前面进行比较。
    设置一个flag判断第一次比较后是否产生交换,如果没有,则说明已经有序。

public void Sort(int[] nums) {
	int len=nums.length();
	int flag=1;
	for(int i=0;i<len;i++){
		for(int j=i+1;j>=0;j--){
			if(nums[j]<num[j-1]){
				swap(nums[j],nmus[j-1]);
				flag=0;
			}
			if(flag)
				break;
		}
	}
}

改进后的算法,对于有序数组只需要进行n次比较。
时间复杂度:最好O(n) 最坏O(n2)

4.希尔排序

    对于数组[3,5,2,4,1],包含逆序(5,2),(5,4),(5,1),(2,1),(4,1),插入排序每次只交换相邻元素,使逆序数量减1,对于大规模的数组,排序速度很慢。希尔排序就是为了解决插入排序的局限性,通过交换不相邻的元素,每次使逆序数量减少大于1。

希尔排序:

希尔排序

public void sort(int[] nums) {
	int len=nums.length();
	int h=len/3;
	while(h>0){
		for(int i=0;i<len;i++){
			for(int j=i+h;j>=h;j=j-h){
				if(nums[j]<num[j-h])
					swap(nums[j],nmus[j-h]);
			}
		}
		h=h/3;
	}
}

    这个代码不觉得似曾相识的样子吗,就是在插入排序的基础上,把每次+1相邻比较换成了每次+h个比较,然后增加了外层循环来改变h的值。因此时间复杂度与插入排序时一样的。
时间复杂度:最好O(n) 最坏O(n2)
空间复杂度:O(1)
稳定性:不稳定

5.归并排序

    将数组分为两部分,分别进行排序,然后归并起来。

拆分:

拆分

归并:

归并

5.1归并方法
public void Merge(int[] nums,int start,int mid,int end){
	int[] temp;
	int i=start,j=mid+1,k=0;
	for(int i=0;i<end;l++)//构建辅助数组
		temp[i] = nums[i];
	while(i<=mid&&j<=end){
		if(nums[i]<=nums[j])//=保证稳定性
			temp[k++] = nums[i++]
		else
			temp[k++] = nums[j++];
	}
	if(i>mid){
		while(j<=end)
			temp[k++] = nums[j++];
	}
	else{
		while(i<=mid)
			temp[k++] = nums[i++];
	}
	for(int i=0;i<end;i++)//归并结果复制回nums
		nums[i] = temp[i];
}
5.2自顶向下归并排序
public void Up2DownMergeSort(int[] nums,int start,int end) {
	if(start>=end)
		return;
	int mid = (strat + end) / 2;
	Up2DownMergeSort(start,mid);
	Up2DownMergeSort(mid+1,end);
	Merge(nums,start,mid,end);
}

    归并排序每次都将问题对半分成两个子问题,这种对半分的算法复杂度一般为 O(nlogn)。
时间复杂度:最好O(nlogn) 最坏O(nlogn)
空间复杂度:O(n)
稳定性:稳定

5.3自底向上归并排序

    从单个元素开始向上成对归并。

public void Down2UpMergeSort(int[] nums) {
	int len=nums.length();
	int lo=2;
	while(lo<=len){
		for(int i=0;i<len;i=i+lo){
			int j = i + lo -1;
			int mid = (i + j) / 2;
			Merge(nums,i,mid,j);			
		}
		lo = lo * 2;
	}
}

6.快速排序

    快速排序在每一轮挑选一个基准元素,让比它大的元素移到右边,比它小的元素移到左边,一般取序列的第一个或最后一个元素作为基准。

快速排序:

快速排序
    例如[4,7,6,5,3,2,8,1],以4为基准,从右边找到第一个比4小的,从左边找到第一个比4大的,交换。

public void QuickSort(int[] nums,int start,int end) {          
	if(start>=end)
		return;    
	int pos =  Partition(nums,start,end);
	QuickSort(nums,start,pos-1);
	QuickSort(nmus,pos+1,end);
}

 public int  Partition(int[] nums,int start,int end){
 	int flag = nmus[start];
    int left = start + 1 ;
    int right = end;
    
    while(left<right){
    	while(left<=end&&nums[left]<flag)
    		left++;
    	while(right>=0&&nums[right]>flag)
    		right--;
    	if(left<=right)
    		swap(nums[left++],nums[right--]);
    }
    swap(nums[start],nums[right]);
    return right;
 }

    快速排序的时间复杂度,一次划分要从两头开始搜索,直到low>=high,所以时间复杂度是O(n),整个排序算法的时间复杂度取决于划分的次数。

  • 理想的情况是,每次划分所选择的中间数恰好将当前序列恰好等分,经过log2n次划分,就可得到长度为1的子表。这样整个算法的时间复杂度为O(nlog2n)。
  • 最坏的情况是,每次划分所选择的中间数恰好是最大或最小数,这样长度为n的数据表的快速排序需要经过n趟划分,退化成了冒泡排序。此时整个算法的时间复杂度为O(n2)。

时间复杂度:最好O(nlogn) 最坏O(n2)
空间复杂度:O(logn)
稳定性:不稳定

6.1算法改进

1.切换到插入排序
    对于很小和部分有序的数组快速排序没有插入排序效果好,而快速排序在小数组中会递归调用自己,因此,在待排序序列的长度分割到一定大小后,可以切换到插入排序。
2.随机选取基准
    前面提到,如果待排序数组是有序数组,每次取序列第一个元素作为基准就退化成了冒泡排序,效率低下,为了缓解这种情况,可以每次从序列中随机选取一个元素作为基准。
3.三数取中
    虽然随机选取基准减少了不好分割的几率,但如果待排序数组元素值全相等时,仍然是O(n2),为了缓解这种情况引入了三数取中。我们知道理想的情况是每次划分的中间数将当前序列等分,最佳的状态是选择序列排序后的中间值,但这很难算出来。一般的做法是选取序列头、中间、尾三个元素排列后的中间值作为基准。
4.三向切分
    对于有大量重复元素的数组,可以将数组切分为三部分,小于、等于、大于,也就是说在一次切分结束后,可以把与基准相同的元素聚集在一起,下一次切分时,不在对与基准相同的元素进行切分。
    例如[3,1,3,2,3,5,3,7,3]以第一个元素3为基准
    第一趟快排结果为[3,1,3,2,3,5,3,7,3],切分成两个子序列[3,1,3,2]和[5,3,7,3]
三向切分第一趟快排结果为[1,2,3,3,3,3,3,7,5],切分成两个子序列[1,2]和[7,5]
对比可见,三向切分能减少迭代次数,提高效率。

public void QuickSort(int[] nums,int start,int end) {      
	int left = start;
	int l = start+1;
	int right = end;
	int flag = nums[start];
	while(l<=right){
		if(nums[l]<flag)
			swap(nums[l++],nums[left++]);//小于基准的数始终在跟基准交换,可以l++
		else if(nmus[l]>flag)
			swap(nmus[l],nums[right--]);//大于基准的数在跟右边的数交换,不知大小,所以不能l++
		else
			l++;
	}
}
6.2快速选择算法

    快速排序的 Partition()函数会返回一个j,使得a[0,j-1]小于a[j],a[j+1,len-1]大于a[j],因此,a[j]就是数组的第j大元素,可以利用这个函数找出数组的第j个元素。

public int select(int[] nums, int k) {
    int l = 0, h = nums.length - 1;
    while (h > l) {
        int j =  Partition(nums, l, h);
        if (j == k) 
            return nums[k];
        else if (j > k) 
            h = j - 1;
        else 
            l = j + 1;        
    }
    return nums[k];
}

7.堆排序

7.1堆

    堆中某个节点的值总是大于等于其子节点的值,并且堆是一棵完全二叉树。
    堆可以用数组来表示,因为堆是一棵完全二叉树,而完全二叉树很容易用数组表示,位置k的节点的父节点在k/2位置,子节点在2k和2k+1位置。为了更清晰的描述节点的位置关系,这里不适用数组索引为0表示。

堆:

堆

7.2上浮和下沉

    在构建大顶堆时,当一个节点比父节点大时,需要交换这两个节点,交换后的节点可能仍然比父节点大,需要不断的比较和交换,把这种操作称为上浮。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

private void swim(int k) {
    while (k > 1 && heap(k / 2) < heap(k)) {
        swap(heap(k / 2), heap(k));
        k = k / 2;
    }
}

    类似的,在构建大顶堆时,当一个节点的值比子节点小,也需要不断向下进行比较和交换,称为下浮。如果一个节点有两个子节点,应该和两个子节点中值较大的节点进行交换。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

private void sink(int k) {
    while (2 * k <= N) {
        int j = 2 * k;
        if (j < N && heap(j) < heap(j + 1))
            j++;
        if (heap(k) >= heap(j))
            break;
        swap(heap(k) , heap(j));
        k = j;
    }
}
7.3插入元素

    将插入元素放到数组的末尾,然后上浮到合适位置。

public void insert(int v) {
    heap[++N] = v;
    swim(N);
}
7.4删除最大元素

    将数组顶端元素删除,将数组最后一个元素放到顶端,然后下沉到合适位置。

public int delMax() {
    int max = heap[1];
    swap(heap(1), heap(N--));
    heap[N + 1] = null;
    sink(heap(1));
    return max;
}
7.5堆排序

    堆排序的基本思想:将待排序序列构造成一个大顶堆,此时整个序列的最大值就是堆顶的根节点,将其与末尾元素进行交换,此时末尾为最大值,然后将剩余N-1个元素重新构造成一个大顶堆,这样会得到N的元素的第二大值,如此反复执行,便能得到一个有序序列了。

7.5.1构造堆

    无序数组建立堆最直接的方式是从左到右(从上到下顺序遍历)进行上浮操作,最后构建为一个大顶堆,但是考虑当一个节点有子节点,而且有子节点的子节点,当它与它的子节点调整后,它可能仍然需要继续调整,那么继续调整之后可能会需要二次调整。
    例如,第一步7,9交换,第二步7,11交换,9,11交换,之后9,10需要二次调整。
在这里插入图片描述
    一个更高效的方式是从右到左(从下往上遍历)进行下沉操作,最后构建为一个小顶堆,如果一个节点的两个节点已经堆有序,下沉可以使以这个节点为根节点的堆有序,此时就算有二次调整也只关子节点,无关父节点。叶子节点不用下沉,从最后一个非叶子节点开始。
    索引从1开始时,最后一个非叶子节点的索引为节点总数/2
在这里插入图片描述

7.5.1交换堆顶元素与最后一个元素

    交换之后需要进行下沉操作维持堆的有序状态。
在这里插入图片描述
    继续交换下沉
在这里插入图片描述
    继续交换下沉
在这里插入图片描述
    继续交换下沉
在这里插入图片描述
    至此,堆排序完成。

public void HeapSort(int[] nums) {
        int N = nums.length;
        for (int k = N / 2; k >= 1; k--)//数组从索引1开始,从最后一个非叶子节点开始构建大顶堆
            sink(nums, N, k);
        while (N > 1) {
            swap(nums[1], nums[N--]);
            sink(nums, N, 1);
        }
    }
private void sink(int[] nums,int N,int k) {
    while (2 * k <= N) {//节点与它的父节点交换后,可能需要与子节点二次调整
        int j = 2 * k;
        if (j < N && nums[j] < nums[j + 1])
            j++;
        if (nums[k] >= nums[j])
            break;
        swap(nums[k] , nmus[j]);
        k = j;
    }
}

    因为堆排序无关乎初始序列是否已经排序已经排序的状态,始终有两部分过程

  • 构建初始的大顶堆的过程时间复杂度为O(n)
  • 交换及重建大顶堆的过程中,需要交换n-1次,重建大顶堆的过程根据完全二叉树高度为logn向下取整的性质,[log2(n-1),log2(n-2)…1]逐步递减次交换
  • 一共近似为nlogn,所以它最好和最坏的情况时间复杂度都是O(nlogn)

时间复杂度:最好O(nlogn) 最坏O(nlogn)
空间复杂度:O(1)
稳定性:不稳定

8.对比表格

算法时间复杂度空间复杂度稳定性
选择排序最好O(n2) 最坏O(n2)O(1)不稳定
冒泡排序最好O(n) 最坏O(n2)O(1)稳定
插入排序最好O(n) 最坏O(n2)O(1)稳定
希尔排序最好O(n) 最坏O(n2)O(1)不稳定
归并排序最好O(nlogn) 最坏O(nlogn)O(n)稳定
快速排序最好O(nlogn) 最坏O(n2)O(logn)不稳定
堆排序最好O(nlogn) 最坏O(nlogn)O(1)不稳定
  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值