1、内部排序
指将需要处理的所有数据都加载到内部存储器中进行排序。
2、外部排序
数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
冒泡排序也属于内部排序法,属于交换排序
。
通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。
int[] nums = new int[]{79,56,90,4,32,27,8};
①第一轮,把最大的数沉到最下面。
79与56相比,大于56,交换
79与90相比,相等,不变
90与4相比,大于4,交换
90与32相比,大于32,交换
90与27相比,大于27,交换
90与8相比,大于8,交换
②第二轮,把第二大的数沉到倒数第二。
56与79相比,小于,不变
79与4相比,大于,交换
79与32相比,大于,交换
79与27相比,大于,交换
90与8相比,大于,交换
③第n轮,以此类推…
public class BubbleSortDemo {
public static void main(String[] args) {
int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
System.out.println("排序前:");
for (int num : nums) {
System.out.print(num + " ");
}
bubbleSort(nums);
System.out.println("\n排序后:");
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 冒泡排序算法
* @param nums 要排序的数组
*/
public static void bubbleSort(int[] nums){
boolean isSort = false; // 在一次流程中是否排过序
int temp = 0; // 临时变量
for (int i = 0; i < nums.length; i++){
for (int j = 0; j < nums.length - 1 - i; j++){
if (nums[j] > nums[j + 1]){ // 如果前面的数大于后面的数,交换
temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
// 因为排序发生过变动,所以需要继续排序
isSort = true;
}
}
if (isSort){
// 发生了变动,重置,继续排序
isSort = false;
}else{
// 没有发生变动,说明已经排好序了
break;
}
}
}
}
快速排序(Quicksort)是对冒泡排序的一种改进,属于交换排序
。
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
int[] nums = new int[]{11,21,28,12,5,8,30};
public class quickSortDemo {
public static void main(String[] args) {
int a = (int) Math.ceil(9 / 2);
System.out.println(a);
int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
System.out.println("排序前:");
for (int num : nums) {
System.out.print(num + " ");
}
quickSort(nums,0,nums.length - 1);
System.out.println("\n排序后:");
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 快速排序算法
* @param nums 要排序的数组
* @param left 数组最左端记录的下标
* @param right 数组最右端记录的下标
*/
public static void quickSort(int[] nums, int left, int right){
if (left > right){
// 不合理
return;
}
// 定义两个变量(哨兵)来接管left和right,left和right后面会用得到
int l = left;
int r = right;
// 定义一个参考值,这里以最右端的作为参考值
int reference = nums[l];
// 定义一个临时变量,用于交换使用
int temp = 0;
while (l != r){ // 只要两个哨兵没有碰头就继续找
// 参考值取的是左边的,则让右边的先找比参考值小的数
while(nums[r] >= reference && l < r){
// 没找到,继续往前找
r--;
}
// 右边已经找到了,让左边的去找比参考值大的数
while(nums[l] <= reference && l < r){
// 没找到,继续往后找
l++;
}
// 两个都找到了,就互相交换值之后,再继续找,直到碰面
temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
}
// 循环结束,表明l和r已经碰面(找到同一个数字上去了)
// 这时就要将碰面的这个值与参考值交换
temp = nums[l];
nums[l] = nums[left];
nums[left] = temp;
// l和r碰面的地方将原本数组分割成两个,再对这两个进行上诉操作,直到无法操作就完成排序了
// 也就是说碰面的地方已经放到了属于他应该在的位置,不用管了
quickSort(nums,left,l - 1);
quickSort(nums,r + 1,right);
}
}
直接插入排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的,属于插入排序
。
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
int[] nums = new int[]{4,56,79,32,27,8,90};
初始状态,就默认第一个数已经插入好了(即在有序表里)。
第1次插入,第二个数和有序表(即图中红色框起来的数据)里的做对比,有序表里找不到比这个数大的,所以不变。第2次也是如此。
第3次插入,32比79小,79后移,32又比56小,56后移。32比4大,找到了位置了。即下标为1的地方。
第n-1次插入,依次类推…
总的来说,就是把一个数组分成两个,一个有序表,一个无序表。初始阶段,有序表就一个元素,无序表n-1个元素。把无序表里的每个元素拿出来,插入到有序表里去。
public class InsertSortDemo {
public static void main(String[] args) {
int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
System.out.println("排序前:");
for (int num : nums) {
System.out.print(num + " ");
}
insertSort(nums);
System.out.println("\n排序后:");
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 插入排序算法
* @param nums 要排序的数组
*/
public static void insertSort(int[] nums){
int insertVal = 0; // 要插入的数据
int insertIndex; // 要插入的位置
for (int i = 1; i < nums.length; i++){
insertVal = nums[i]; // 当前要插入的数据
insertIndex = i - 1; // 要插入的位置,肯定在这个要插入数据的前面找
/**
* 1、要保证不越界,如果insertIndex==0,表示插入到最前端
* 2、其次保证前面没有数比这个更大
*/
while(insertIndex >= 0 && insertVal < nums[insertIndex]){ // 开始找插入的位置
// 至少找到一个比这个数要大的,那么就把比这个数大的数往后移动
nums[insertIndex + 1] = nums[insertIndex];
insertIndex--; // 继续往前找
}
// 退出循环,表明再也找不到比这个数大的了,即找到了要插入的位置
if((insertIndex + 1) != insertIndex){ // 如果要插入的位置就是自己,则表明顺序是对的,无需赋值
nums[insertIndex + 1] = insertVal;
}
}
}
}
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序
,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序
。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
int[] nums = new int[]{4,56,79,32,27,8,90,65,30,63};
public class ShellSortDemo {
public static void main(String[] args) {
int a = (int) Math.ceil(9 / 2);
System.out.println(a);
int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
System.out.println("排序前:");
for (int num : nums) {
System.out.print(num + " ");
}
shellSortByExchange(nums);
System.out.println("\n排序后:");
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 希尔排序算法
*
* @param nums 要排序的数组
*/
public static void shellSortByExchange(int[] nums) {
// 第一轮循环,确定间隔(增量)/分了几组
for (int gap = nums.length / 2; gap > 0; gap /= 2){
// 分了几组,就对几组进行操作
for(int i = 0; i < gap; i++){ // 这里的i,其实就是每一个组的第一个元素
for (int j = i + gap; j < nums.length; j++){ // 直接插入法
int insertIndex = j - gap; // 要插入的位置
int insertValue = nums[j]; // 要插入的位置
while(insertIndex >= 0 && insertValue < nums[insertIndex]){
nums[insertIndex + gap] = nums[insertIndex]; // 后移
insertIndex -= gap; // 继续往前找
}
// 退出循环,表示已经找到
if((insertIndex + gap) != j){ // 如果要插入的位置就是自己,则表明顺序是对的,无需赋值
// 这里为什么要insertIndex + gap呢?
// 因为在前面的循环里,找到了插入的下标,但是它认为不是最合适的,所以继续往前找
// 结果不符合循环条件了,但是下标却往前移动了,要移动回来才对
nums[insertIndex + gap] = insertValue;
}
}
}
}
}
}
简单选择排序(select sorting)也是一种简单的排序方法,属于选择排序
。
第一次从nums[0] ~ nums[n-1] 中选取最小值,与 nums[0] 交换,第二次从 nums[1]~nums[n-1] 中选取最小值,与 nums[1] 交换,第三次从 nums[2] ~ nums[n-1] 中选取最小值,与nums[2]交换,…,第i次从 nums[i-1] ~ nums[n-1] 中选取最小值,与nums[i-1]交换,…, 第n-1次从 nums[n-2] ~ nums[n-1] 中选取最小值,与nums[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
int[] nums = new int[]{79,56,4,32,27,8,90};
①第一轮,把最小值找出来,放到最上面。
79与56相比,新的最小值56
56与4相比,新的最小值4
4与32相比,最小值不变
4与27相比,最小值不变
4与8相比,最小值不变
4与90相比,最小值不变
完成一轮比较,最小值是4,与第一个数字进行交换。
②第二轮,从后面的数中找出最小值,放在后面的数的最上面(全局第二)。
56与79相比,最小值不变,为56
56与32相比,新的最小值32
32与27相比,新的最小值27
27与8相比,新的最小值8
8与90相比,最小值不变
③第n-1轮,依次类推…
public class SelectSortDemo {
public static void main(String[] args) {
int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
System.out.println("排序前:");
for (int num : nums) {
System.out.print(num + " ");
}
selectSort(nums);
System.out.println("\n排序后:");
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 选择排序算法
* @param nums 要排序的数组
*/
public static void selectSort(int[] nums){
for (int i = 0; i < nums.length - 1; i++){
int minIndex= i; // 最小值的下标
int minNum = nums[i]; // 最小值
for (int j = i + 1; j < nums.length; j++){
if(minNum > nums[j]){ // 找到比最小值还要小的
// 重置最小值及其下标
minNum = nums[j];
minIndex = j;
}
}
// 一轮比较完毕,找到真正的最小值
if (minIndex != i){ // 最小值下标不是当前这个数的下标,即做交换
nums[minIndex] = nums[i];
nums[i] = minNum;
}
}
}
}
归并排序(MERGE-SORT)是利用归并
的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略
(分治法将问题分(divide)
成一些小的问题然后递归求解,而治(conquer)
的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
public class MergeSortDemo {
public static void main(String[] args) {
int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
System.out.println("排序前:");
for (int num : nums) {
System.out.print(num + " ");
}
int[] temp = new int[nums.length];
mergeSort(nums,0,nums.length - 1,temp);
System.out.println("\n排序后:");
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 归并排序算法 - 分+治
* @param nums 要排序的数组
* @param left 数组最左端
* @param right 数组最右端
* @param temp 临时数组 - 需要额外开辟一个空间作为辅助
*/
public static void mergeSort(int[] nums, int left, int right, int[] temp){
if (left < right){
// 求出中间下标
int mid = (left + right) / 2;
mergeSort(nums, left, mid, temp);
mergeSort(nums,mid + 1, right, temp);
merge(nums, left, mid, right, temp);
}
}
/**
* 合并算法
* @param nums 原数组
* @param left 数组最左端
* @param mid 数组中间下标
* @param right 数组最右端
* @param temp 临时数组 - 需要额外开辟一个空间作为辅助
*/
public static void merge(int[] nums, int left, int mid, int right, int[] temp){
// 定义两个变量接管left和right
int l = left; // 即左边有序序列的当前索引
int r = mid + 1; // 即右边有序序列的当前索引
int t = 0; // 指向temp数组的当前索引
// 1、先把左右有序序列按照规则填充到temp数组,直到有一个有序序列处理完毕
while(l <= mid && r <= right){
if (nums[l] <= nums[r]){ // 左边有序序列的当前元素小于等于右边有序序列当前元素的情况
temp[t] = nums[l];
t++; // t下标移动
l++; // l下标移动
}else{ // 右边有序序列的当前元素小于左边有序序列当前元素的情况
temp[t] = nums[r];
t++;
r++;
}
}
// 2、上面循环完毕,表示有一个序列里的数据已经完全被填充到temp里了,这时只需把没有填充完的那个序列填充到temp即可
// 左边数组还有剩余元素,将左边元素全部填充到temp里
while(l <= mid){
temp[t] = nums[l];
t++;
l++;
}
// 右边数组还有剩余元素,将左边元素全部填充到temp里
while(r <= right){
temp[t] = nums[r];
t++;
r++;
}
// 3、将辅助数组temp里的元素拷贝回nums数组
// 注意:这里不能直接nums = temp,这是分治,并不是最后一次
// 如果nums = temp,就会导致后续的递归出问题
t = 0;
int tempLeft = left;
while (tempLeft <= right){
nums[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
}
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。
1、基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法。
2、基数排序(Radix Sort)是桶排序的扩展
3、基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
int[] nums = new int[]{79,56,4,32,27,16,88,35,32};
public class RadixSortDemo {
public static void main(String[] args) {
int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
System.out.println("排序前:");
for (int num : nums) {
System.out.print(num + " ");
}
radixSort(nums);
System.out.println("\n排序后:");
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 基数排序算法
* @param nums 要排序的数组
*/
public static void radixSort(int[] nums){
// 获取数组中最大数的位数
int max = nums[0]; // 先默认第一个就是最大的
for (int i = 1; i < nums.length; i++){
if (max < nums[i]){
max = nums[i];
}
}
// 最大数的位数
int maxLength = (max + "").length();
// 十个桶,防止溢出的情况(个/十....位全是同一个数字的情况),二维数组大小为原数组大小
int[][] bucket = new int[10][nums.length];
/**
* 用于记录每个桶中放了多少个数据
* 比如:bucketElementCounts[0]就是记录的bucket[0]这个桶中放入的数据个数
*/
int[] bucketElementCounts = new int[10];
for (int i = 0, n = 1; i < maxLength; i++, n *= 10){
// n表示的就是要取个十百千万位...等用到的除数
for (int j = 0; j < nums.length; j++){
// 取出原数组中的每个值对应位的值
int digitOfElement = nums[j] / n % 10;
// 放到对应的桶里去
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = nums[j];
bucketElementCounts[digitOfElement]++; // 这个桶里的数据个数+1
}
// 从桶里取出来,放回原数组
int index = 0;
for (int k = 0; k < bucketElementCounts.length; k++){
if (bucketElementCounts[k] != 0){ // 表明这个桶里有数据
// 循环遍历这个桶,取出数据放入原数组
for (int l = 0; l < bucketElementCounts[k]; l++){
nums[index++] = bucket[k][l];
}
}
// 桶的计数置0
bucketElementCounts[k] = 0;
}
}
}
}
1、基数排序是对传统桶排序的扩展,速度很快.
2、基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
3、基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
4、有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考: 基数排序为负整数
1、堆排序是利用堆
这种数据结构而设计的一种排序算法,堆排序是一种选择排序
,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
。
2、堆是具有以下性质的完全二叉树
:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆
注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
3、每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
4、大顶堆举例说明
我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子:
大顶堆特点:
arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2] // i 对应第几个节点,i从0开始编号
arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2] // i 对应第几个节点,i从0开始编号
1、将待排序序列构造成一个大顶堆
2、此时,整个序列的最大值就是堆顶的根节点。
3、将其与末尾元素进行交换,此时末尾就为最大值。
3、然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.
int[] nums = new int[]{20,10,45,15,35,25,50,40,30};
public class HeapSortDemo {
public static void main(String[] args) {
int[] nums = new int[]{20,10,45,15,35,25,50,40,30,65,7,3,1,85,46,96,32,20,48,73};
System.out.println("排序前:");
for (int num : nums) {
System.out.print(num + " ");
}
heapSort(nums);
System.out.println();
System.out.println("排序后:");
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 堆排序算法
* @param nums 要排序的数组
*/
public static void heapSort(int[] nums){
for (int i = nums.length / 2 - 1; i >= 0; i--){
// 构建大顶堆,从第一个非叶子节点开始,从下往上 + 从右往左
adjustHeap(nums,i,nums.length);
}
// 调整完毕之后,交换堆顶元素和末尾元素
int temp = 0;
for (int i = nums.length - 1; i > 0; i--){
temp = nums[0];
nums[0] = nums[i];
nums[i] = temp;
// 交换完毕之后,重新对堆进行调整
adjustHeap(nums,0,i);
}
}
/**
* 调整大顶堆
* @param nums 要调整的数组
* @param i 非叶子节点在数组中的索引
* @param length 对多少个元素继续调整
*/
public static void adjustHeap(int[] nums, int i, int length){
int temp = nums[i]; // 当前调整的非叶子节点
// 从左子节点开始,依次往下调整
for (int k = i * 2 + 1; k < length; k = k * 2 + 1){
if (k + 1 < length && nums[k] < nums[k + 1]){
// 如果左子节点比右子节点小,k指向右子节点
k++;
}
if (nums[k] > temp){
// 如果子节点大于父节点,则将子节点赋值给父节点
nums[i] = nums[k];
// 其实这两步可以省略,加上这个配合图更好理解
nums[k] = temp;
temp = nums[k];
i = k; // 父节点更新
}else{ // 表明已经是调整好了的
break;
}
// 一轮结束,没有退出循环,以新的父节点开始,继续调整
}
}
}
此外,我还做了一个测试,80000条数据,各个排序各用时间(在同一环境下,电脑比较渣,所以时间都比较长):
冒泡排序:13037毫秒
快速排序:57毫秒
直接插入排序:696毫秒
希尔排序:4694毫秒
选择排序:2712毫秒
归并排序:17毫秒
基数排序:20毫秒
堆排序:10毫秒
当然,排序不能仅仅根据时间来判定好坏。要根据场景,选择合适的排序方式。