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

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

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 = GetPos(nums,start,end);
	QuickSort(nums,start,pos-1);
	QuickSort(nmus,pos+1,end);
}

 public int GetPos(int[] nums,int start,int end){
 	int flag = nmus[start];
    int left = start + 1 ;
    int right = end;
    
    while(left<right){
    	while(nums[left]<flag)
    		left++;
    	while(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快速选择算法

    快速排序的GetPos()函数会返回一个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 = GetPos(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) 不稳定
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章