算法之排序问题

前言

  排序问题是一个老生常谈的问题,网上关于排序问题的博客一大堆,还讲解的比较透彻生动,但是每次看博客的时候就像走马观花,很快就忘记了。因此此篇博客主要是用来加深自己的记忆和理解,本篇博客的内容大部分是基于前人的博客上进行所写的。文末会附上讲解排序算法比较好的博客链接。

常见排序算法

冒泡排序

  冒泡排序的过程就像水中的一个气泡一样,轻的的上浮,沉的下降。其具体思路是 将相邻两个位置上的元素进行比较,如果前面的元素比后面的元素大,就交换位置,如果比它小就保持不变,元素位置+1
冒泡排序示意图
  在上图中第一趟进行比较的过程为:arr[0]与[1],arr[1]与arr[2],arr[2]与arr[3],arr[3]与arr[4],比较4次,在第一次排序最大的值到了数组末尾,其排序后结果如下:
第一次排序结果
  第二趟arr[0]与[1],arr[1]与arr[2],arr[2]与arr[3],比较3次;
  第三趟arr[0]与[1],arr[1]与arr[2],比较2次;
  第四趟arr[0]与[1],比较1次。
  代码实现时,双层for循环,外层控制冒泡轮数里层依次比较。时间复杂度为O(n²),是稳定排序。

public static void sort(int[] arr){
	for(int i = 0;i < arr.length ;i++){
		for(int j = 0;j < arr.length-i;j++){
			int temp = 0;
			if(arr[i] > arr[j]){
				temp = arr[i];
				arr[i] = arr[j];
				arr[j] = temp;
			}
		}
	}
}

选择排序

  选择排序的思路为每次比较的时候,用一个索引位置上的元素依次与其他索引位置上的元素比较,当该元素比当前元素大时,交换位置,否则当前元素位置+1,继续比较。也可以将思路理解为:首先,找到数组中最小的元素,拎出来,将它和数组的第一个元素交换位置,第二步,在剩下的元素中继续寻找最小的元素,拎出来,和数组的第二个元素交换位置,如此循环,直到整个数组排序完成
选择排序
  第一次arr[0]依次与arr[1],arr[2],arr[3],arr[4]比较,比较4次,其结果如下:
第一趟排序结果
  第二趟arr[1]依次与arr[2],arr[3],arr[4],比较3次
  第三趟arr[2]依次与arr[3],arr[4],比较2次
  第四趟arr[3]依次与arr[4],比较1次。
  代码实现时,双层for循环,第一层控制比较趟数,第二层控制每趟比较的次数。时间复杂度为O(n²),是不稳定排序。

public static void sort(int[] arr){
	for(int i = 0; i < arr.length-1;i++){
		int min = i;
		for(int j = i+1; j < arr.length;j ++){
			int temp = 0;
			if(arr[min] > arr[j]){
				min = j;
            }
		}
		int temp = arr[i];
		arr[i] = arr[min];
		arr[min] = arr[i];
	}
}

快速排序

  此处代码与原理参考的博客为快速排序(转载)
  快速排序的思路为:核心思想是分治法,分而治之。每趟从数组中选择一个值作为基准值,其他数依次和基准值做比较,比基准值大的元素放右边,小的放左边,然后再对左边和右边的两组数分别选出一个基准值key,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列。
  每趟排序的方法如下:
1.定义i=0,j = arr.length-1,i为第一个数的下标,j为数组最后一个数的下标;
2.从数组的最后一个数arr[j]从右往左找,找到第一个小于key的数,记为arr[j];
3.从数组的第一个数arr[i]从左往右找,找到第一个大于key的数,记为arr[i];
4.交换arr[i]和arr[j];
5.重复上述步骤直到i==j;
6.调整key的位置把key和arr[i]交换。则该趟排序过程完成。
  假设要排序的数组为arr[8]={5,2,8,9,2,3,4,9},key=5,i=0,j=7
每趟快速排序的过程
代码如下:

public static void quickSort(int[] arr){
	if(arr.length > 0){
		quickSort(arr,0,arr.length-1);
	}
}
public static void quickSort(int[] arr,int low,int high){
	//1.找到递归算法的入口
	if(low > high){
		return ;
	}
	//排序前的初始化
	int i = low;//i
	int j = high;//j
	int key = arr[low];//key
	//2.完成一趟排序
	while(i < j){
		//2.1从右往左找到第一个小于key的数
		while(i < j && arr[j] >= key){
			j--;
		}
		//2.2从左往右找到第一个大于key的数
		while(i < j && arr[i] < key){
			i++;
		}
		//2.3交换
		if(i < j){
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;
		}
	}
	//2.4调整key的位置
	int p = arr[i];
	arr[i] = arr[low];
	arr[low] = p;
	//3.对key左边的数快排
	quickSort(arr,low,i-1);
	//对key右边的数进行快排
	quickSort(arr,i+1,high);
}

  代码实现时,在遍历数组时,新增两个指针从数组两边向中间逼近,把左边大于key值的数与右边小于key值的数进行交换,直到两个指针指向同一个下标时,将该下标对应的值与key值交换,一趟排序即完成。时间复杂度为O(n*logn),是不稳定排序。

插入排序

  插入排序的思路为:每趟将一个数插入到已排好序的序列中,直到所有的数都排好序为止。参考链接
1.第一趟时,从第二个数开始处理。默认将第一个数作为已经排好序的数据;当第二个数大于第一个数时将该数排在第一个数后面一个位置,否则将第二个数放在第一个数前面。此时前两个数新城一个有序的数据。
2.第二趟排序时,从第三个数开始处理。此时前两个数有序,当第三个数大于第二个数时,将第三个数放在第二个数后面一个位置,并结束此次循环;否则再和第一个数比较,如果第三个数大于第一个数,则第三个数插入第一个数和第二个数中间,否则第三个数小于第一个数,则将第三个数放在第一个数前面。此时前三个数形成了一个有序的数据。
3.后面的数据处理方式同上,直至结束。
  举例如下
插入排序过程

在这里插入图片描述
接上面
  数组剩余数的排序过程的原理如上所示。代码如下所示:

public static void insertSort(int[] arr){
	if(arr == null || arr.length == 0){
		return ;
	}
	for(int i = 1; i < arr.length;i ++){
		//当arr[i]比arr[i -1]小时才进行处理
		if(arr[i] < arr[i-1]){
			//临时变量存储arr[i]的值
			int temp = arr[i];
			//从当前位置开始处理
			int j = i;
			//将比temp大的数往后挪一个位置,为temp腾出一个合适的位置
			while(j > 0 && temp < arr[j-1]){
				arr[j] = arr[j-1];
				j--;//填充完后,继续向前比较
			}
			//将temp放在属于它的位置上
			arr[j] = temp;
		}
	}
}

  插入排序就是每趟都将数组左半部分有序的数据个数加1,有点像打牌理牌的过程。其时间复杂度为O(n^2),是稳定的排序算法。

堆排序

  此部分堆排序的原理过程参考博客,就不做详细的流程了。附上参考博客的链接。堆排序(转载)
  基本思想为:将待排序序列构成一个大顶堆,此时整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构成一个堆,这样会得到n个元素的次小值。如此反复执行便能得到一个有序序列了。
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
  a.假设给定无序序列结构如下
在这里插入图片描述
2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
在这里插入图片描述
4.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
在这里插入图片描述
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
在这里插入图片描述
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
在这里插入图片描述
b.重新调整结构,使其继续满足堆定义
在这里插入图片描述
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
在这里插入图片描述
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
在这里插入图片描述
再简单总结下堆排序的基本思路:
  a.将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
代码如下:

public static void heapSort(int[] arr){
	//1.构建大顶堆
	for(int i = arr.length/2-1;i >= 0;i--){//第一个非叶子节点的下标为arr.length/2-1
		//从第一个非叶子节点从下至上,从右至左调整结构
		addjustHeap(arr,i,arr.length);
	}
	//2.调整堆结构+交换堆顶元素与末尾元素
	for(int j = arr.length-1;j >0 ;j--){
		swap(arr,0,j);//将堆顶元素与末尾元素进行交换
         adjustHeap(arr,0,j);//重新对堆进行调整
	}
}
/**
     * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
     * @param arr要排序的数组
     * @param i 当前要调整的结点
     * @param length 数组的长度
     */
    public static void adjustHeap(int []arr,int i,int length){
        int temp = arr[i];//先取出当前元素i
        for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
        /*这个if语句块实现了找出左右子节点中,值最大的那个节点的下标*/
            if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
                k++;
            }
            if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
                arr[i] = arr[k];
                i = k;
            }else{
                break;
            }
        }
        arr[i] = temp;//将temp值放到最终的位置
    }

    /**
     * 交换元素
     * @param arr
     * @param a
     * @param b
     */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}

  堆排序就是从第一个非叶子节点开始,比较其与其左右子节点的大小,若不满足条件,则将该节点与对应的子节点交换位置,同时继续比较该子节点与其孩纸节点的关系,直到要比较的节点为叶子节点。堆排序时间复杂度一般认为就是O(nlogn)级。是不稳定排序。

归并排序

  归并排序的思路为:采用的是分而治之的思想,将大问题分解成子问题进行递归处理,然后再将每个子问题的解组合在一起得到最终的结果。适用于处理较大规模的数据,缺点是较耗内存。
  该方法的思路跟过程(图跟代码)参考别人的博客,附上链接归并排序(转载)
  假设无序数组arr={8,4,5,7,1,3,6,2},其分治的过程就是先将数组一直均分,直到子数组只剩一个元素(默认一个元素是有序的),然后再对每个子数组进行排序,并将排序后的结果进行合并即可得到最终的结果。
分而治之
分治总过程
  分阶段可以看成是递归拆分子序列的过程,治阶段就是将两个已经有序的子序列合并成一个有序序列。
合并相邻有序子序列
  比如将上图中的子序列[4,5,7,8]与[1,2,3,6]合并过程如下:

合并子序列
  在合并子序列,同时遍历两个子序列,逐个比较子序列中的值,将值小的那个数存入辅助数组中,并将下标加1,继续比较,直到气质一个子序列的下标为数组长度,将下标小的子序列剩余的数直接复制到辅助数组中,则排序完成。
  代码如下:

public static void sort(int[] arr){
	int[] temp = new int[arr.length];//在排序前先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
	sort(arr,0,arr.lrngth-1,temp);
}
private static void sort(int[] arr,int left,int right,int[] temp){
	if(left < right){
		int mid = (left + right)/2;
		sort(arr,left,mid,temp);//左边归并排序使左子序列有序
		sort(arr,mid,right,temp);//右边归并排序,使右子序列有序
		merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
	}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
	int i = left;//左序列指针
	int j = mid + 1;//右序列指针
	int t = 0;//临时数组指针
	while(i <= mid && j<= right){
		if(arr[i] < arr[j]){
			temp[t] = arr[i];
			t++;
			i ++;
		}else{
			temp[t++]=arr[j++];
		}
	}
	while(i <= mid ){//将左边剩余元素填进temp数组中
		temp[t++]=arr[i++];
	}
	while(j <= right){//将右边剩余元素填进temp中
		temp[t++]=arr[j++];
	}
	t=0;
	//将temp中元素全部拷贝到原数组中
	while(left <= right){
		arr[left++] = temp[t++];
	}
}

  归并排序是稳定排序,其时间复杂度为O(nlogn).

计数排序

  计数排序的思路是:利用一个额外的空间如数组C来统计数组中每个数出现的次数,其中数组C中的第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。算法的具体过程如下:
1.找出待排序数组中最大和最小的元素;
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
3.对所有计数累加(从C中的第一个元素开始,每一项和前一项相加);
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
:后面几个排序算法的思路与代码都是从博客排序算法总述(转载) 中复制粘贴过来的,纯粹是为了加深自己的理解和记忆,如有维权请联系本人。
  代码如下:

public static int[] CountingSort(int[] arr){
	if(arr.length == 0) return arr;
	int bias,min = arr[0],max=arr[0];
	for(int i = 1; i<arr.length;i++){
		if(arr[i]> max)//找数组中最大值的过程
			max = arr[i];
		if(arr[i] < min)//找数组中最小值的过程
			min = arr[i];
	}
	bias = 0-min;//计算0与最小值之间的差值,用于后续辅助数组的下标与带排序数组的值相对应
	int[] bucket = new int[max-min+1];
	arr.fill(bucket,0);
	for(int i =0;i <arr.length;i++){
		bucket[arr[i] + bias]++;//统计待排序数组中每个值出现的次数
	}
	int index = 0,i=0;
	while(index < arr.lenth){
		if(bucket[i]!=0){//根据辅助数组的结果来对待排序数组进行排序
			arr[index] = i -bias;
			bucket[i]--;
			index ++;
		}else{
			i++
		}
	}
	return arr;
}

   计数排序是复杂度为O(n+k)稳定的排序算法,k是待排序列最大值,适用在对最大值不是很大的整型元素序列进行排序的情况下(整型元素可以有负数,我们可以把待排序列整体加上一个整数,使得待排序列的最小元素为0,然后执行计数排序,完成之后再变回来。这个操作是线性的,所以计数这样做计数排序的复杂度仍然是O(n+k))。本质上是一种空间换时间的算法,如果k比较小,计数排序的效率优势是很明显的,当k变得很大的时候,这个算法可能就不如其他优秀的排序算法。

桶排序

  桶排序的思路是:桶排序工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间。参考链接[桶排序(转载)] (https://blog.csdn.net/developer1024/article/details/79770240)
实现逻辑
1.设置一个定量的数组当作空桶子。
2.寻访序列,并且把项目一个一个放到对应的桶子去。
3.对每个不是空的桶子进行排序。
4.从不是空的桶子里把项目再放回原来的序列中。
稳定的,时间复杂度为O(n+k)
  代码待续。。。。

希尔排序

  希尔排序的思路是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。原理及代码为转载,链接如下希尔排序
  按照定义得到的希尔排序,代码如下:

void shellSort(int[] arr){
	int i,j,gap;
	int n = arr.length;
	for(gap = n/2;gap> 0;gap = gap/2){//步长
		for(i=0;i<gap;i++){//直接插入排序
			for(j = i+gap;j< n;j+=gap){
				if(arr[j] > arr[j-gap]){
					int temp = arr[j];
					int k = j-gap;
					while(k >=0 && arr[k]>temp){
						arr[k+gap] = arr[k];
						k-=gap;
					}
					arr[k+gap] = temp;
				}
			}
		}
	}
}

  希尔排序是不稳定的,时间复杂度为O(nlogn)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值