算法与数据结构 — 排序算法,含 Java 高质量算法实现

算法与数据结构 — 排序算法,含 Java 高质量算法实现

一、概念

  1. 稳定性:
    假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

二、排序算法分类

在这里插入图片描述

三、时间复杂度

排序算法(平均)时间复杂度(最好)时间复杂度(最坏)时间复杂度空间复杂度稳定性
比较排序:
插入排序O(n2)O(n) (基本有序)n2O(1)稳定
希尔排序不稳定
选择排序O(n2)O(n2)O(n2)O(1)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
冒泡排序O(n2)O(n)O(n2)O(1)稳定
快速排序O(nlogn)O(nlogn)O(n2)O(nlogn)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
非比较排序:
计数排序O(n + k)O(n + k)O(n + k)O(n + k)稳定
桶排序O(n + k)O(n)O(n2)O(n + k)稳定
基数排序O(n * k)O(n * k)O(n * k)O(n + k)稳定

四、排序算法详解

1. 直接插入排序

  1. 算法思想:
    第 i 趟插入排序为:在含有 i − 1个元素的 有序子序列 中插入一个元素,使之成为含有 i 个元素的有序子序列。在查找插入位置的过程中,同时后移元素,所以适合从后向前扫描。
    整个过程为进行 n − 1 趟插入, 即先将整个序列的第 1个元素看成是有序的,然后从第 2个元素起,逐个进行插入,直到整个序列有序 为止。

  2. 算法实现:

    /**
     * 直接插入排序算法实现 O(n2)
     * 在含有 i − 1个元素的有序子序列中插入一个元素,使之成为含有 i 个元素的有序子序列
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] insertSort(int[] arr) {
    	// 假定第一个元素被放到了正确的位置上
    	for (int i = 1; i < arr.length; i++) {
    		// 待排序元素
    		int target = arr[i];
    		int preIndex = i - 1;
    
    		while (preIndex >= 0 && arr[preIndex] > target) {
    			// 后移元素
    			arr[preIndex + 1] = arr[preIndex];
    			preIndex--;
    		}
    		// 找到插入位置之后,将第 i 个元素插入到有序序列中
    		arr[preIndex + 1] = target;
    	}
    	return arr;
    }
    
  3. 算法优化:

    注意到插入排序的过程中,每次是将第 i 个元素插入到前面有序序列中,既然是插入到有序序列中,那么可以采用二分查找的思想来寻找第i个元素插入到前面有序序列中的位置,这样优化之后的算法时间复杂度为O(nlogn)。优化之后的算法:

    public int[] insertSortV2(int[] nums) {
    	for (int i = 1; i < nums.length; i++) {
    		int current = nums[i];
    
    		// 采用二分查找,查找插入排序第i个元素在前面有序序列中的位置 pivot
    		int low = 0, mid = 0, high = i - 1;
    		int pivot = 0;
    		while (low <= high) {
    			mid = (low + high) / 2;
    			if (current < nums[mid]) {
    				high = mid - 1;
    			}
    			if (current >= nums[mid]) {
    				low = mid + 1;
    			}
    		}
    		// 找到插入位置之后,将第 i 个元素插入到有序序列中
    		pivot = low;
    		for (int j = i; j > pivot; j--) {
    			nums[j] = nums[j - 1];
    		}
    		nums[pivot] = current;
    	}
    	return nums;
    }
    
  4. 测试平台:LeetCode:912. 排序数组
    1是使用最原始的插入排序的算法,2是使用优化之后的插入排序的算法
    在这里插入图片描述


2. 希尔排序

  1. 算法思想:
    希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。

  2. 过程演示:
    在这里插入图片描述

  3. 算法实现:

    /**
     * 希尔排序算法实现
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] shellSort(int[] arr) {
    	int len = arr.length;
    	// 设置默认增量为:n/2
    	int gap = len / 2;
    
    	while (gap > 0) {
    		for (int i = gap; i < len - 1; i++) {
    			int current = arr[i];
    			int preIndex = i - gap;
    
    			while (preIndex >= 0 && arr[preIndex] > current) {
    				arr[preIndex + gap] = arr[preIndex];
    				preIndex -= gap;
    			}
    			arr[preIndex + gap] = current;
    		}
    		gap /= 2;
    	}
    	return arr;
    }
    

3. 选择排序

  1. 算法思想:
    首先 在 整个排序序列 中找到最小(大)元素,存放到排序序列的 起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到 已排序序列的末尾 。以此类推,直到所有元素均排序完毕。

  2. 算法实现:

    /**
     * 选择排序算法实现 O(n2)
     * 从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾已排序序列的末尾
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] selectSort(int[] arr) {
    	// min记录 每趟排序过程中 最小值的下标
    	int minIndex;
    	for (int i = 0; i < arr.length; i++) {
    		minIndex = i;
    		for (int j = i + 1; j < arr.length; j++) {
    			// 未排序序列中选择最小的元素
    			if (arr[j] < arr[minIndex]) {
    				minIndex = j;
    			}
    		}
    		// 一趟排序结束,选择最小的值放到已排序的第 i 的位置上
    		if (minIndex != i) {
    			int temp = arr[i];
    			arr[i] = arr[minIndex];
    			arr[minIndex] = temp;
    		}
    	}
    	return arr;
    }
    

4. 堆排序

  1. 概念:

    • 堆排序:
      堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
    • 堆:
      堆是具有以下性质的 完全二叉树 :每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
      构建的 (大/小) 顶堆,二叉树的根是整个序列的 最(大/小)值,堆排序正是利用的是这个特性。
  2. 算法思想:

    • 步骤1:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区
    • 步骤2:将 堆顶元素R[1]最后一个元素R[n] 交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
    • 步骤3:由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1) 调整为新堆 ,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
  3. 算法实现:


5. 冒泡排序

  1. 算法思想:

    首先将第 1个元素和第 2个元素进行比较,若前者大于后者,则两者交换位置,然后比较 第 2个元素和第 3个元素。依此类推,直到第 n − 1个元素和第 n个元素进行过比较或交换为止。上 述过程称为 一趟冒泡排序,其结果是使得 n个元素中值最大的那个元素被安排在最后一个元素的位置 上。然后进行第二趟排序,即对前 n − 1个元素进行同样的操作,使得前 n − 1个元素中值最大的那 个元素被安排在第 n − 1个位置上。

    一般地,第 i 趟冒泡排序是从前 n − i + 1个元素中的第 1个元素 开始,两两比较,若前者大于后者,则交换,结果使得前 n − i + 1个元素中最大的元素被安排在第 n − i + 1个位置上。

  2. 优化:
    显然,判断冒泡排序结束的条件是“在一趟排序中没有进行过交换元素的操作”, 为此,设立一个标志变量 flag,flag = 1表示有过交换元素的操作,flag = 0表示没有过交换元素的操 作,在每一趟排序开始前,将 flag置为 0,在排序过程中,只要有交换元素的操作,就及时将 flag置 为 1。因为至少要执行一趟排序操作,故第一趟排序时,flag = 1。

  3. 算法实现:
    以下提供的两个函数,皆为冒泡算法的实现。
    bubbleSort 为 原始的冒泡排序,bubbleSortV2为加入flag优化过之后的冒泡排序。
    大家可以在第二个循环内加入count来测试以下优化前后两个算法的差异,可以感受到第二个函数循环执行的次数要少于第一个。

    /**
     * 冒泡排序算法实现
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] bubbleSort(int[] arr) {
    	int len = arr.length;
    	for (int i = 0; i < len; i++) {
    		for (int j = 0; j < len - 1 - i; j++) {
    			if (arr[j + 1] < arr[j]) {
    				int temp = arr[j + 1];
    				arr[j + 1] = arr[j];
    				arr[j] = temp;
    			}
    		}
    	}
    	return arr;
    }
    
    /**
     * 冒泡排序函数实现,通过判断一趟冒泡排序中,位置是否发生变化来终止
     *
     * @param arr 待排序数组
     * @return
     */
    public static int[] bubbleSortV2(int[] arr) {
    
    	// 判断一趟冒泡排序过程中,是否发生交换,如果没有发生交换,则代表序列已经有序。
    	// flag = 1:发生交换,flag = 0:无交换
    	int flag = 1;
    
    	for (int i = 0; i < arr.length && flag == 1; i--) {
    		flag = 0;
    		for (int j = 0; j < arr.length - i - 1; j++) {
    			if (arr[j] > arr[j + 1]) {
    				int temp = arr[j];
    				arr[j] = arr[j + 1];
    				arr[j + 1] = temp;
    				flag = 1;
    			}
    		}
    	}
    	return arr;
    }
    

6. 快速排序

  1. 算法思想:

    在序列中任意选择一个元素(通常称为分界元素或 基准 元素),把小于或等于基准的所有元素都移到基准的前面,把大于基准的所有元素都移到基准的后面, 这样,当前序列就被划分成前后两个子序列,其中前一个子序列中的所有元素都小于后一个子序列的所有元素,并且基准正好处于排序的最终位置上。然后分别对这两个子序列递归 地进行上述排序过程,直到所有元素都处于排序的最终位置上,排序结束。
    快速排序的本质:每趟排序将选择的基准放到正确的位置。

  2. 算法实现:

    /**
     * 快速排序:递归实现
     *
     * @param arr
     * @param low
     * @param high
     */
    public void quickSort(int[] arr, int low, int high) {
    	if (low < high) {
    		// 找寻基准数据的正确索引
    		int pivotIdx = getPivotIndex(arr, low, high);
    
    		// 进行迭代对 基准之前和之后的数组进行相同的操作使整个数组变成有序
    		quickSort(arr, low, pivotIdx - 1);
    		quickSort(arr, pivotIdx + 1, high);
    	}
    }
    
    
    /**
     * 快速排序算法 -- 基准位置
     * 一趟快排,将基准放到正确的位置,即基准左边的数都 <= 基准,即基准右边的数都 >= 基准
     *
     * @param arr
     * @param low
     * @param high
     * @return
     */
    public int getPivotIndex(int[] arr, int low, int high) {
    	// 以序列中第一个元素作为基准数据
    	int pivot = arr[low];
    	while (low < high) {
    		// 当队尾的元素大于等于基准数据时,向前挪动high指针
    		while (low < high && arr[high] >= pivot) {
    			high--;
    		}
    
    		// 如果队尾元素小于基准,需要将其赋值给low
    		arr[low] = arr[high];
    
    		// 当队首元素小于等于基准数据时,向前挪动low指针
    		while (low < high && arr[low] <= pivot) {
    			low++;
    		}
    
    		// 当队首元素大于基准数据时,需要将其赋值给high
    		arr[high] = arr[low];
    	}
    
    	// 跳出循环时low和high相等,此时的low或high就是基准的正确索引位置
    	arr[low] = pivot;
    
    	// 返回基准的正确位置
    	return low;
    }
    

7. 归并排序

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。

  1. 算法思想:

    归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。

    将数组平均分成两部分: center = (left + right)/2,当数组分得足够小时—数组中只有一个元素时,只有一个元素的数组自然而然地就可以视为是有序的,此时就可以进行合并操作了。因此,上面讲的合并两个有序的子数组,是从 只有一个元素 的两个子数组开始合并的。


8. 计数排序

  1. 算法思想:
    计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为 存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是 有确定范围的整数

    当输入的元素是 n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。

  2. 算法实现:

    /**
     * 计数排序算法实现
     *
     * @param arr
     * @return
     */
    public static int[] countingSort(int[] arr) {
    	if (arr.length == 0) {
    		return arr;
    	}
    
    	// min,max:确定用于计数的数组的大小,计数数组大小为:(max - min + 1)
    	int min = arr[0], max = arr[0];
    	for (int i = 1; i < arr.length; i++) {
    		if (arr[i] < min) {
    			min = arr[i];
    		}
    		if (arr[i] > max) {
    			max = arr[i];
    		}
    	}
    	int[] bucket = new int[max - min + 1];
    
    	// 统计序列中各个元素出现的次数count
    	for (int i = 0; i < arr.length; i++) {
    		int bias = arr[i] - min;
    		// 第 i 个元素频次加1
    		bucket[bias]++;
    	}
    
    	// 把计数数组统计好的数据汇总到原数组
    	int index = 0;
    	for (int i = 0; i < bucket.length; i++) {
    		for (int j = 0; j < bucket[i]; j++) {
    			arr[index] = min + i;
    			index++;
    		}
    	}
    	return arr;
    }
    

9. 桶排序

  1. 算法思想:
    桶排序就是把最大值和最小值之间的数进行瓜分,例如分成 10 个区间,10个区间对应10个桶,我们把各元素放到对应区间的桶中去,再对每个桶中的数进行排序,可以采用归并排序,也可以采用快速排序之类的。
    之后每个桶里面的数据就是有序的了,我们在进行合并汇总。
  2. 算法演示:
    在这里插入图片描述

10. 基数排序

  1. 算法思想:
    基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,时间复杂度为O(kn), n为数组长度,k为数组中的数的最大的位数;

    基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

  2. 算法实现:

    /**
     * 基数排序算法实现
     *
     * @param arr 待排序序列
     * @return
     */
    public static int[] radioSort(int[] arr) {
    	if (arr == null || arr.length < 2) {
    		return arr;
    	}
    
    	int n = arr.length;
    	int max = arr[0];
    	// 找出最大值
    	for (int i = 1; i < n; i++) {
    		max = Math.max(max, arr[i]);
    	}
    	// 计算最大值是几位数
    	int num = 1;
    	while (max / 10 > 0) {
    		num++;
    		max = max / 10;
    	}
    	// 创建10个桶
    	ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10);
    	// 初始化桶
    	for (int i = 0; i < 10; i++) {
    		bucketList.add(new LinkedList<Integer>());
    	}
    	// 进行每一趟的排序,从个位数开始排
    	for (int i = 1; i <= num; i++) {
    		for (int j = 0; j < n; j++) {
    			// 获取每个数最后第 i 位是数组
    			int radio = (arr[j] / (int) Math.pow(10, i - 1)) % 10;
    			//放进对应的桶里
    			bucketList.get(radio).add(arr[j]);
    		}
    		//合并放回原数组
    		int k = 0;
    		for (int j = 0; j < 10; j++) {
    			for (Integer t : bucketList.get(j)) {
    				arr[k++] = t;
    			}
    			//取出来合并了之后把桶清光数据
    			bucketList.get(j).clear();
    		}
    	}
    	return arr;
    }
    
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值