Java 排序算法

排序算法

排序的分类

按主要操作分类

  • 比较排序:用比较的方法
    • 插入排序:直接插入排序、折半插入排序、希尔排序
    • 交换排序:冒泡排序、快速排序
    • 选择排序:简单选择排序、堆排序
    • 归并排序:2-路归并排序
  • 基数排序:不比较元素大小,仅仅根据元素本身的取值确定其有序位置

按辅助空间分类

  • 原地排序:辅助空间用量为O(1)的排序方法
  • 非原地排序:辅助空间用量超过O(1)的排序方法

按稳定性分类

当对多关键字的记录序列进行LSD方法排序时,需要采用稳定的排序方法

  • 稳定排序:数值相等的元素,排序后相对次序不变
  • 非稳定性排序

按自然性分类

  • 自然排序:输入数据越有序,排序速度越快的排序方法
  • 非自然排序

排序的性能

类别 排序方法 说明
插入排序 直接插入排序 适合原本就有序(顺序)和短数组
交换排序 冒泡排序 适合本身就有序的(正序)
快速排序 不适合本身有序的数组,如果本身就有序就会退化成冒泡排序
选择排序 简单选择排序 与初始状态无关
堆排序 与初始状态无关,适合元素个数比较多
归并排序 与初始状态无关

image

交换排序

基本思想:两两比较,如果发生逆序则交换,直到所有记录都排好序位置

冒泡排序

基于简单交换思想

基本思想:每趟不断将记录两两比较,并按"前小后大"规则交换。
每一躺确定一个最大的元素,所以n个元素需要n-1躺
每一趟比较的次数 = n - 趟数。

image

class Solution {
    public int[] sortArray(int[] nums) {
	int n = nums.length;
	int tmp;
	for(int m=1;m<=n-1;m++){ //m表示趟数
		boolean flag = true;
		for(int i=0;i<n-m;i++){ //i表示比较次数
			if(nums[i]>nums[i+1]){ //如果发生逆序交换
				tmp = nums[i+1];
				nums[i+1] = nums[i];
				nums[i] = tmp;
				flag = false;
			}
			if(flag) return nums; //优化:如果某一趟没有元素交换,说明已经排好序了。
		}
	}
    return nums;
    }
}

优点
每趟结束时,不仅能让最大值找到位置,还能同时部分理顺其他元素。

优化
如果某一趟比较时不出现记录交换,说明已经排好序了,可以提前计数了

算法分析
最好情况(正序)
比较次数:n-1
移动次数:0

最坏情况(逆序)
比较次数:1+2+...n-1
移动次数:3(比较次数)

最好时间复杂度:O(n)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(n^2)

快速排序

基本思想
1.任取一个元素(如第一个)为中心
2.所以比中心小的往前放,比中心大的元素向后方,以中心元素为基准分成了左右两个子表。(小 中 大)
3.对各个子表重新选择中心元素并依此规则调整
4.直到每一个子表只剩下一个元素

通过一趟排序,将排序的数组分隔成了两个部分,基准元素也找到最终的位置。
子表的形成是采用从两头向中间交替逼近法
一个n个元素,那么就需要n-1趟。

使用递归三部曲

递归的参数、返回值和停止的条件

快速排序是改进的交换算法,在排序的过程中交换元素,所以是直接对数组进行排序,不需要返回值。
一趟排序后将数组分成了左右两个部分,左右两个部分继续进行相同的排序方法,所以我们需要用两个指针来划分数组的区间。

void QSort(int [] nums,int left,int right){
	//递归终止条件
	if(left>=right)return;
}

本层递归逻辑

本层递归需要先找到基准点的最终位置,根据基准点的位置将数组分成两部分的位置。
然后左右两部分继续按规则调整。

void QSort(int [] nums,int left,int right){
	//递归终止条件长度大于1才开始划分
	if(left < right){ //如果基准点在最后一个元素,那么从下一轮递归中index+1>right了,用只有一个元素left==right判断就会出问题
	//找基准点的最终位置,返回索引值。
		int index = partiton(nums,left,right); 
		QSort(nums,left,index-1);
		QSort(nums,index+1,right);
	}
}

int  partiton(int [] nums,int left,int right){
	int pivotkey = nums[left]; //假设数组的第一个元素为基准点
	//右边的指针先行,如果找到比基准点小的就可以去覆盖左边指针位置的值,而左边指针位置的值就是我们的基准点,已经被存起来了
	while(left<right){
	while(left<right && nums[right]>=pivotkey)--right; //从后往前找比基准点小的值放前面
	nums[left] = nums[right];
	while(left<right && nums[left]<=pivotkey)++left;//从前往后找比基准点大的值放后面
	nums[right] = nums[left];
	} 
	//退出循环说明找到了找到了
	nums[left] = pivotkey;
	return left;
}

优化:随机选取基准值

可以降低初始顺序对快速排序效率的影响

1.随机选取一个下标
1.1 范围在[left,right]之间,需要生成[left,right]之间的随机整数
2.该下标的值与第一个元素交换

需要生成[left,right]之间的随机整数

random.nextInt(int n) 该方法的作用是生成一个随机的int值,该值介于[0,n)
Math.random() 随机 返回[0,1)随机数

假设需要[2,6]之间的随机整数

方法1:使用random.nextInt(int n)

Random random = new Random();
random.nextInt(right - left+1); //随机一个int值[0,5)
random.nextInt(right - left+1) +left; //随机一个int值[2,7)	

方法2:使用Math.random()

Math.random()*(right-left+1)  //[0,5)的随机数
(int)Math.random()*(right-left+1) //[0,5)的随机整数
left+(int)Math.random()*(right-left+1) //[2,7)的随机整数
Random random = new Random();
int randomIndex = left + 1 + random.nextInt(right - left+1);
int tmp = nums[left];
nums[left]= nums[randomIndex];
nums[randomIndex] = emp;
int pivot  = nums[left];

算法分析

  • 时间复杂度 O(nlogn)

快速排序是内排序(不适用外部内存的排序)中最快的一个

QSort()递归的深度使logn:O(logn)
partiton():O(n)

  • 空间复杂度 O(logn)

快速排序不是原地算法

平均情况下:O(logn)的占空间
最坏情况下:占空间可达O(n)

  • 稳定性 不稳定

快速排序是一种不稳定的排序算法

  • 是否自然排序 不是自然排序

分别对逆序或顺序的数组进行快速排序,每一次划分之后得到的子序列1的长度为0,这时退化成为冒泡排序时间复杂度O(n^2)

快速排序不适合对原本有序或基本有序的记录序列进行排序

输入的数据次序越乱,所划分元素值的随机性越好,排序速度越快,快速排序不是自然排序方法

划分元素的选取是影响时间性能的关键。

归并排序

归并排序,当我们需要排序一个数组时,先将数组分成一半,然后想办法把左边的数组给排序,右边的数组给排序,之后再将它们归并起来。那现在对数组的排序就变成了对左边数组的排序和右边数组的排序。

当然了当我们对左边的数组和右边的素组进行排序的时候,再分别将左边的数组和右边的数组分成一半,然后对每一个部分归并。如图:

image

什么时候停止递归,只有一个元素时已经有序了,就不用再排序了,直接归并就好了。

这个思路不就是递归的思路吗!归并的过程就是在递归。归并排序是稳定排序

归并算法的实际复杂度 O(nlogn),递归的深度是logn 合并的的时间复杂度是n

使用递归三部曲解题

递归的参数和返回值

我们这个返回void直接对nums进行修改。
参数需要原数组和两个划分区间的指针left、right

 void merge (int[]nums,int left,int right){
 }

递归的终止条件
只有一个元素时,自然就是有序的

if(left==right) return;

本层递归需要做什么

本层递归需要把有序的两个部分合并成有序的一个部分。

public int[] sortArray(int[] nums) {
     int len = nums.length;
     merge(nums,0,len-1);
     return nums;
}

void merge (int[]nums,int left,int right){
if(left>=right) return; 
int mid = (left+right)>>1; //除以2的操作
 merge(nums,left,mid); //左边的有序数组
 merge(nums,mid+1,right);//右边的有序数组
//!!!!!!!如果数组的这个子区间本身有序,无需合并,这里是一个优化!!!!
if (nums[mid] <= nums[mid + 1]) {
   return;
}
//对l1和l2进行合并
mergeSort(nums,left,mid,right); //左右两边有序合成一个有序数组
}

//合并两个
 void mergeSort(int[] nums,int left,int mid,int right){
        int l = left, m = mid +1;
        int k = 0;
        int [] arr = new int[right-left+1]; //记录排好序的结果
	 	
        while(l<=mid && m<=right){
			//相同元素原来靠前的排序以后依然靠前,所以这里必须写成<=才能保证稳定性
            if(nums[l]<=nums[m]) arr[k++] = nums[l++];
            else arr[k++] = nums[m++];
        }
	 	//把左右两个数组剩下的元素放入数组
        while(l<=mid){
            arr[k++] = nums[l++];
        }
	       while(m<=right){
            arr[k++] = nums[m++];
        }

        for(int i=0;i<arr.length;i++){ //新数组覆盖老数组
            nums[left+i] = arr[i]; 
        }
    }

复杂度分析

  • 时间复杂度:O(NlogN),这里 N 是数组的长度;
  • 空间复杂度:O(N),辅助数组与输入数组规模相当

插入排序

基本思想(类似扑克牌):每步将一个待排序的对象,按其大小插入到前面已经排好序的数组中的适当位置,直到对象全部插入为止。
边插入边排序,保证子序列中随时都是排好序的。

image

如何找到插入位置?根据寻找插入位置的不同,将插入排序分成3类

  • 顺序法定位插入位置:直接插入排序
  • 二分法定位插入位置: 二分插入排序
  • 缩小增量多遍插入顺序:希尔排序

直接插入排序

1.先把i位置的值存起来tmp
2.顺序查找插入位置,可以从后往前也可以从前往后,选择从后往前,边找边移动

class Solution {
    public int[] sortArray(int[] nums) {
       for(int i = 1; i < nums.length; i++){
            int tmp = nums[i];
            for(int j = i - 1; j >= 0; j--){
                if(tmp < nums[j]){
                    nums[j + 1] = nums[j]; //元素后移
                    nums[j] = tmp;
                }else{//找到了插入位置
                    break;
                }
            }
        }
        return nums;
    }
}

优化
「将一个数字插入一个有序的数组」这一步,可以不使用逐步交换,使用先赋值给「临时变量」,然后「适当的元素」后移,空出一个位置,最后把「临时变量」赋值给这个空位的策略。

最好的情况,从小到大有序(顺序)

比较的次数:n个元素排序,n-1个需要和前面有序的进行比较
移动次数:0

提高查找速度

  • 减少元素的比较次数
  • 减少元素的移动次数

折半插入排序

查找插入位置时采用折半查找法,查找待排序元素位置时前面已经有序了,所以可以使用折半查找

 public void insertSort(int nums[]){
	 int tmp;
       for(int i = 0; i < nums.length; i++){ 
		   int low=0,high=i-1;
            tmp = nums[i];//tmp待排序元素
       		while(low<high){
				int mid = (low+high)>>1;
				if(nums[mid]>tmp) high = mid-1;
				else low = mid+1;
			}//找到插入位置,high+1为插入位置
		    for(int j=i-1;j>=high+1;--j)nums[j+1] = nums[j];
		    nums[high+1] =tmp; 
            }
        }
		return nums;
    }

查找时的比较次数与待排序对象的初始化排列无关。在插入第i个对象时,需要经过logi+1次比较

当n较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比最好情况要差

  • 减少了比较次数,但没有较少移动次数
  • 平均性能优于直接插入排序

复杂度分析

  • 时间复杂度O(n^2)
  • 空间复杂度O(1)
  • 稳定排序

希尔排序

image

基本思想
将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录基本有序,再对全体记录进行一次插入排序

特点

  • 缩小增量,增量序列递减,最后以一个必须是1,也就是对全体记录进行一次插入排序
  • 多遍插入排序
  • 一次移动,移动位置较大,跳跃式地接近排序后的最终位置

image

1.选择增量序列 D3=5,D2=3,D1=1
2.进行DK-间隔插入排序,每隔DK个的元素一起进行排序

算法分析

希尔排序算法效率与增量序列的取值有关
是不稳定的排序算法

选择排序

堆排序

堆的定义

image

堆实质使满足 二叉树中任意非叶子结点均小于(大于)它的孩子节点的 完全二叉树(除叶子节点都每一层都满,叶子节点从左到右填充)

image

在大根堆/小根堆 中堆顶是最大值(最小值),输出堆顶元素后,调整堆,依次输出堆 顶就是排好序的。
堆排序需要解决两个问题:

  • 如何由一个无序序列建成一个堆?
  • 如何输出堆顶元素后,调整剩余元素为新堆?

堆在很多语言被称为优先队列

调整新堆

大根堆为例

1.输出堆顶元素后,以堆中最后一个元素替代

//index:需要调整的下标
//end:待排序列尾元素索引
void adjust(int []nums, int index,int end){
	while(2 * index + 1<=end){ //左节点
		 int tmp = nums[index];
		int max = 2*index+1; //max记录大的子节点,暂时让它为左节点
		if(max+1<=end && nums[max]<nums[max+1])max++; //如果右孩子节点存在,且右孩子>左孩子,那么max记录右孩子
		if(tmp>nums[max]) break; //如果父节点指比大的孩子节点值还大,直接退出,用于建堆时	
		//说明还没有调整结束,大的孩子节作为根节点
		nums[index] = nums[max] ;
		nums[max] = tmp;
		index = max ; //沿key较大的孩子节点向下筛选
		}
	}

无序序列建成一个堆

从一个无序序列建堆的过程就是一个反复筛选的过程。

在完全二叉树中,左右节点为空的子树是堆(叶子节点),所以叶子节点不用调整,从第一个非叶子节点开始调整。

最后一个元素为n,也就是最后一个叶子节点为n,那么第一个不为叶子节点的节点就是它的父节点n/2。
只需要依次以序号n/2....1的结点为根的子树均调整为堆即可。

private void heapify(int[] nums) {
        int len = nums.length;
        // 从(n-1)/2 因为节点从0开始
        for (int i = (len - 1) / 2; i >= 0; i--) {
            adjust(nums, i, len - 1);
        }
    }

堆排序

输出堆顶,然后重新排序

public int[] sortArray(int[] nums) {
//建初始堆
int len = nums.length;
int i;
for (i= (len - 1) / 2; i >= 0; i--) {
   adjust(nums, i, len - 1); //建初始堆
}
//输出栈顶元素,根与最后一个元素交换,大根堆,从大到小输出
int tmp;
for(i=len-1;i>1;i--){//进行n-1躺排序,n个元素,堆顶
	swap(nums,0,i); //根和待排序的最后一个元素交换,相当于把堆顶元素输出
	adjust(nums,0,i-1);
}
return
}
void swap(int[]nums,int a,int b){ //a,b是局部变量不能直接交换
	int tmp = nums[a];
	nums[a]=nums[b];
	nums[b]=tmp;
}

性能分析

  • 初始化阶段所需时间不超过O(n)
  • 排序阶段(不含初始堆化)
    • 一次重新堆化所需时间不超过O(logn)
    • n-1次循环所需时间不超过O(nlogn)

堆排序很稳定,不管正序逆序(与初始状态无关),最好最坏时间复杂度都是O(nlogn)

简单选择排序

在待排序的数组中选出最大/小的元素放在最终位置

基本操作
1.首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换
2.在通过n-2次比较,从剩余的n-1个记录中找出关键字最小的消极,与第二个记录交换
3.重复上述操作,共进行n-1躺排序后(因为每躺排序好一个元素),排序结束。

public class Solution {

    public int[] sortArray(int[] nums) {
        int len =  nums.length;
        int min;
       for(int i=0;i<len;++i){
	    min =i ; //记录最小值下标
     	for(int j=i+1;j<len;j++){
		    if(nums[j]<nums[min]) min = j;
	    }
	    if(min!=i){
            //如果第一个元素不是最小值则交换
            int tmp = nums[min];
            nums[min] = nums[i];
            nums[i] = tmp;
        }
        }
        return nums;
        }
}

时间复杂度分析
移动次数:
最好情况:0
最坏情况:3(n-1)
比较次数:
都需要从前到后比较,n-1+n-2...+1

posted @ 2021-12-28 18:21  rananie  阅读(85)  评论(0编辑  收藏  举报