Java(4)-十大排序算法

更好的总结:RUNOOB.COM 十大经典排序算法

冒泡排序

冒泡排序的基本思想是比较数组中相邻的两个元素,根据比较结果交换它们的位置,让较大的元素排到数组末尾。
遍历过程:

  1. 首轮遍历:从数组的第一个元素开始,比较相邻的两个元素,如果前一个元素大于后一个元素,则交换它们的位置,从而第一遍遍历会让最大的元素移动到数组的末端。
  2. 后续重复:忽略已经排序好的末尾元素,对剩余的元素重复上述操作。
  3. 终止条件:不再有新的元素需要交换时。
public static void sort(int[] nums){  
    int n = nums.length;  
    for(int i = 0;i < n - 1;i++){ // i表示完成排序的元素个数,也就是排到末端的个数  
        for(int j = 0;j < n - 1 - i;j++){   
			if(nums[j] > nums[j+1]){  
                int temp = nums[j];  
                nums[j] = nums[j+1];  
                nums[j+1] = temp;  
            }  
        }  
    }  
}

时间复杂度:

  1. 最好情况:O(n),输入数组已经是排序好的状态,遍历一次,不需要进行任何交换
  2. 最坏情况:O($n^2$),数组初始完全逆序
  3. 平均情况:O($n^2$)
    空间复杂度:
    O(1)

选择排序

思路:从未排序的序列中找到最小的元素,将其放在排序序列的起始位置。
举个例子:一组数字 [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5],选择排序的过程如下:

  • 第一轮选择后,1 是这组数字中的最小值,我们将它放在列表的第一个位置,并将原来的第一个位置的数字(3)放到1原来的位置。现在列表看起来是 [1, 3, 4, 1, 5, 9, 2, 6, 5, 3, 5]。
  • 第二轮选择后,从第二个位置开始的最小值是 1,我们将它放在第二个位置,并将原来第二个位置的数字(3)放到1原来的位置。列表更新为 [1, 1, 4, 3, 5, 9, 2, 6, 5, 3, 5]。
  • 如此继续,每一轮都选出最小的数字放到已排序的序列末尾,直到整个数组排序完毕。
public static void sort(int[] nums) {  
    int n = nums.length;  
    for (int i = 0; i < n - 1; i++) {  
        int minIdx = i; // 最小值索引  
        for (int j = i + 1; j < n; j++) {  
            if (nums[j] < nums[minIdx]) {  
                minIdx = j;  
            }  
        }  
        int temp = nums[i];  
        nums[i] = nums[minIdx];  
        nums[minIdx] = temp;  
    }  
}

时间复杂度:
选择排序无论数组初始状态如何,比较次数都是固定的,复杂度都是O($n^2$)
空间复杂度:
O(1)

插入排序

插入排序工作原理类似于整理扑克牌,想象我们手里拿着一些扑克,需要排序。我们可能会从左往右检查每张牌,每找到一张新牌就将其插入到左侧已经排序好的牌中的对应位置。因此插入排序的核心思想就是:将每一个元素插入到已经排序好的序列中的适当位置。

public static void sort(int[] nums) {  
    int n = nums.length;  
    int pos, cur;  
    for (int i = 1; i < n; i++) { // 默认第一张牌是排序好的,所以从第二张开始遍历  
        pos = i - 1;// 已经排序好的牌中最大的那张  
        cur = nums[i]; // 要插入的新牌  
        while (pos >= 0 && cur < nums[pos]) { //查找cur应该插入的位置,从已排序部分的最后一个元素开始,向前遍历  
            nums[pos + 1] = nums[pos]; //由于cur小于nums[pos],我们需要为cur腾出空间  
            pos--;//继续向前寻找,比较cur和前面的元素  
        }  
        // 循环结束后,pos指向了cur应该插入的位置的前一个位置  
        // 因为循环中我们是比较后将pos减少,所以最终pos停留在正确插入点前的一个位置  
        nums[pos + 1] = cur;  
    }  
}

时间复杂度:

  1. 最好情况:O(1),初始数组就是已经排序好的
  2. 最坏情况:O($n^2$),初始数组完全逆序
  3. 平均情况:O($n^2$)
    空间复杂度:
    O(1)

希尔排序

最开始接触希尔排序的时候看那些定义有点不明白,找了个例子可以帮助理解。
想象我们要把一大堆乱放的书籍按照一定的顺序排序好。如果我们每次只拿起一本书,找到它应该放的位置,这样效率可能非常低,特别是书非常多的时候。希尔排序就是在这种情况下提出的一个更聪明的办法。
1. 分批处理:首先,我们不再一本一本地整理,而是分批处理。可能先每隔十本书拿起一本,只把这些书放到正确的位置。这样,虽然整体上书还是乱的,但是每十本书中已经有一本是放对了位置。
2. 逐步精细调整:完成第一轮粗略的排序后,再缩小范围,比如这次每隔五本书处理一本。虽然这次需要处理更多的书,但由于第一轮的预处理,很多书已经不需要移动太远。
3. 最后的细节调整:继续这样逐步减少间隔,直到最后每本书都要考虑一次。这时,由于前面的预处理,整理起来会更快更容易。
通过这种分批次的处理,每次虽然我们移动的书不一定都在最终位置,但是大部分书都离自己的位置更近了。这样到了最后,即使是细节上的调整,也因为大部分书已经不错了,所以整体效率提高了很多。
正式地介绍希尔排序:希尔排序是通过将原始列表分割成多个子列表来提高排序的效率。个子列表包含原始列表中间隔特定"增量"的元素。通过逐步缩小增量,直至增量为1,希尔排序使得元素逐渐移向其正确的位置。
排序过程:
1.选择增量序列:最初的增量通常是数组长度的一半,之后每次将增量减半,直到增量为1。
2.分组排序:在每一个增量的步骤中,我们可以用排序算法进行排序,可以是冒泡也可以是插入等。
3. 缩小增量:完成一轮增量值的插入排序后,减少增量值,并重复上述过程。
4.增量为1的最终排序:当增量减少到1时,整个数组就变成了一个子列表。这时进行一次标准的某某排序算法。

public static void sort(int[] nums) {  
    // 分组 + 冒泡  
    int n = nums.length;  
    int gap = n / 2;  
    while (gap > 0) {  
        for (int j = gap; j < n; j++) {  
            int i = j;  
            while (i >= gap && nums[i - gap] > nums[i]) {  
                int temp = nums[i];  
                nums[i] = nums[i - gap];  
                nums[i - gap] = temp;  
                i -= gap;  
            }  
        }  
        gap /= 2;  
    }  
}

时间复杂度:
1.最好情况:O(nlogn)
2.最坏情况:O($n^2$)
3.平均情况:O($n^{3/2}$)
空间复杂度:
O(1)

归并排序

1.5 归并排序 | 菜鸟教程 非常推荐这里的动图,看完就懂了。
归并排序采用分治法的思想将一个数组分成更小的数组,直到每个小数组只有一个元素,然后将这些数组递归合并成较大数组,直到最终得到一个完全排序的数组。
排序步骤:
1. 分割:首先将数组从中间分割成前后两部分,然后递归分割左右两部分,直到每个部分只有一个元素或为空为止,因为一个元素自然是排好序的。
2. 归并:然后将这些分割后的部分两两归并,归并的过程需要将它们排序。比如,我们有两个已排序的子列表,从两个列表的开始(最小端)比较,选择较小的元素放入新的列表中,然后移动该列表的索引,直到所有元素都重新排序在一起。
例子:
假设有数组 [4, 3, 2, 1],归并排序的过程是:
分割:[4, 3, 2, 1] -> [4, 3] 和 [2, 1]
再分割:[4, 3] -> [4] 和 [3];[2, 1] -> [2] 和 [1]
归并:[4] 和 [3] 归并为 [3, 4];[2] 和 [1] 归并为 [1, 2]
再归并:[3, 4] 和 [1, 2] 归并为 [1, 2, 3, 4]

public static void sort(int[] nums, int start, int end) {  
    if (start < end) {  
        int mid = (start + end) / 2;  
        // 分别排序  
        sort(nums, start, mid);  
        sort(nums, mid + 1, end);  
        // 合并  
        merge(nums, start, end);  
    }  
}  
public static void merge(int[] nums, int left, int right) {  
    // 合并的是两个有序数组  
    int[] temp = new int[nums.length];  
    int mid = left + (right - left) / 2;  
    int p1 = left; // 左边的数组  
    int p2 = mid + 1; // 右边的数组  
    int k = left;  
    while (p1 <= mid && p2 <= right) {//合并相同长度的部分  
        if (nums[p1] <= nums[p2]) {  
            temp[k++] = nums[p1++];  
        } else {  
            temp[k++] = nums[p2++];  
        }  
    }  
    while (p1 <= mid) {//合并左边数组可能多的情况  
        temp[k++] = nums[p1++];  
    }  
    while (p2 <= right) {//合并右边数组可能多的情况  
        temp[k++] = nums[p2++];  
    }  
    for (int i = left; i <= right; i++) {  
        nums[i] = temp[i];  
    }  
}

时间复杂度:
最好最坏情况,时间复杂度都是O(n logn)
空间复杂度:
O(n)

快速排序

快速排序是分治策略,基本步骤:
1.选择基准:从数组中选择一个元素作为基准值
2.分区操作:重排数组,使得所有小于基准值的元素都移到基准左边,大于基准值的元素移到右边
3.递归排序:递归地将小于基准值的部分和大于基准值的部分分别进行快排

public static void sort(int[] nums, int start, int end) {  
    if (start >= end) return;  
    int left = start;  
    int right = end;  
    int temp = nums[left];  
    while (left < right) {  
        while (left < right && temp <= nums[right]) {  
            right--;  
        }  
        nums[left] = nums[right];  
        while (left < right && temp >= nums[left]) {  
            left++;  
        }  
        nums[right] = nums[left];  
    }  
    nums[left] = temp;  
    sort(nums, start, left - 1);  
    sort(nums, left + 1, end);  
}

时间复杂度:
1.最好情况:O(n logn),每次都可以将数组精确分成两个大小几乎相等的子数组
2.最坏情况:O($n^2$),每次选项的基准值都是最大或者最小的值
3.平均情况:O(n logn)
空间复杂度:
1.最坏情况:O(n),每次分区只选到基准值是最大或者最小值的情况,导致每次递归只能减少一个元素,分成的两部分中一部分包含n-1个元素,另一部分只有0个元素
2.平均和最好情况:O(logn),每次分区都将数组分成大致相等的两部分

堆排序

堆本质上是一个完全二叉树,分成最大堆和最小堆:最大堆中,每个节点的值都大于等于其子节点的值;最小堆中,每个节点的值都小于等于其子节点的值。堆排序就是利用了这种数据结构进行排序的算法。
排序步骤:
1.建立堆:
- 将无序的数据数组构造成一个最大堆或最小堆。一般来说,如果想要排序结果是升序的,就建立最大堆;如果想要的是降序的,就建立最小堆。
- 这个过程是从最后一个非叶子节点开始,向前逐个进行下沉调整(让所有子节点都小于或等于父节点)。
2.调整堆
- 将堆顶元素(最大或最小值)与堆的最后一个元素交换,然后移除最后一个元素(这个元素已经是当前最大或最小值了)。
- 对剩下的元素重新进行下沉调整,确保顶部是最大或最小值。
- 重复此过程,直到所有元素都被移除堆,即完成排序。

public static void sort(int[] list) {  
    /**  
     * 构造堆  
     * 从第一个非叶子节点,也就是倒数第二行最后一个开始调整  
     * 左右孩子节点中较大的交换到父节点中  
     * i是从下往上  
     */  
    for (int i = list.length / 2 - 1; i >= 0; i--) {  
        headAdjust(list, list.length, i);  
    }  
    /**  
     * 排序堆  
     * 将最大节点list[0]放在堆尾list[i]  
     * 然后从根节点重新调整  
     * 每次把最后一个排好位置的最大值忽略掉  
     */  
    for (int i = list.length - 1; i >= 1; i--) {  
        int temp = list[0];  
        list[0] = list[i];  
        list[i] = temp;  
        headAdjust(list, i, 0);  
    }  
}  
  
/**  
 * 调整堆  
 *  
 * @param list:整个二叉树  
 * @param len:list的长度  
 * @param i:三个中的根节点  
 */  
public static void headAdjust(int[] list, int len, int i) {  
    int index = 2 * i + 1; // 左孩子  
    while (index < len) {  
        if (index + 1 < len) { // 说明还有右孩子  
            if (list[index] < list[index + 1]) {  
                index = index + 1;  
            }  
        }  
        // 检查是否有右孩子,且右孩子比左孩子大,如果是,更新index为右孩子索引  
        if (list[index] > list[i]) {  
            int temp = list[i];  
            list[i] = list[index];  
            list[index] = temp;  
            i = index;  
            index = 2 * i + 1;  
        } else {  
            break;  
        }  
    }  
  
}

时间复杂度:
最好最坏和平均情况下都是O(n logn)
空间复杂度:
O(1)

计数排序

计数排序,计数计的是数组中每个数字出现的次数。
排序步骤:
1. 初始化:首先,创建一个足够大的数组,大小为待排序数组中的最大值加1(比如1到100的范围,就创建101个格子的数组)。
2. 计数:遍历待排序的数组,每读到一个数字,就在相应的数组索引位置增加1。这样,每个位置的数字就代表了该索引值在原数组中出现的次数。
3. 输出排序结果:最后,遍历这个计数数组,根据每个位置上的数值,重复输出其索引,从而得到一个有序数组。
计数排序适用于数据范围较小并且分布比较均匀的情况,比如成绩排序、调查问卷数据排序等;因此对于数据范围很大或者数据分布很不均匀的情况,就会导致占用过多的内存。

public static void sort(int[] nums, int min, int max) {  
    int n = nums.length;  
    int[] temp = new int[max - min + 1];  
    // 计数  
    for (int i = 0; i < n; i++) {  
        temp[nums[i] - min]++;  
    }  
    int idx = 0;  
    for (int i = 0; i < temp.length; i++) {  
        int cnt = temp[i];  
        while (cnt != 0) {  
            nums[idx] = i + min;  
            idx++;  
            cnt--;  
        }  
    }  
}

时间复杂度:
O(n+k) :n是原数组的长度,k是数据范围的长度
空间复杂度:
O(k)

桶排序

桶排序是计数排序的升级版。桶排序解决了计数排序在处理大范围数据时效率低和空间浪费的问题,并且计数排序仅适用于非负整数,桶排序可以用于整数、浮点数甚至其他数据类型。
想象一下你有一堆乒乓球,每个球上面标有不同的数字,你需要按照数字的大小排序。桶排序的过程就像是你用多个桶来分类这些球:将数字范围划分为几个区间,每个区间对应一个桶,然后根据乒乓球上的数字将它们放入相应的桶中。放好之后,再在每个桶内进行排序,最后依次将每个桶里的球取出,这样就完成了整体的排序。
排序步骤:
1.确定桶的数量和范围:基于待排序数据的范围和特点,选择合适的桶的数量和每个桶的值域范围。
2. 分配元素到桶中:遍历原始数据,根据元素值将每个元素分配到对应的桶中。
3. 对每个桶内部进行排序:可以使用其他排序方法,如快速排序或插入排序。
4. 收集桶中的元素:按顺序从每个桶中取出元素,合并成一个有序的数组。

/**  
 * 以浮点数排序为例  
 */  
public static void sort(float[] nums) {  
    // 1.初始化桶,桶内会频繁插入数据,选用LinkedList  
    ArrayList<LinkedList<Float>> buckets = new ArrayList<LinkedList<Float>>();  
    for (int i = 0; i < 10; i++) {  
        buckets.add(new LinkedList<>());  
    }  
    // 2.数据放入桶中并完成排序  
    for (float num : nums) {  
        int idx = getBucketIdx(num); // 桶序号  
        insertSort(buckets.get(idx), num);  
    }  
    // 3.从桶中取出数据  
    int idx = 0;  
    for (LinkedList<Float> bucket : buckets) {  
        for (Float num : bucket) {  
            nums[idx++] = num;  
        }  
    }  
  
  
}  
  
// 计算元素放入哪个桶中  
public static int getBucketIdx(float num) {  
    // 取浮点数的整数部分作为桶的索引值,实际具体怎么分桶可以变  
    return (int) num;  
}  
  
// 桶内元素进排序,具体怎么排序自己定  
public static void insertSort(List<Float> bucket, float num) {  
    ListIterator<Float> iterator = bucket.listIterator();  
    boolean insertFlag = true;  
    while (iterator.hasNext()) {  
        if (num <= iterator.next()) {  
            iterator.previous();  
            iterator.add(num);  
            insertFlag = false;  
            break;        }  
    }  
    if (insertFlag) {  
        bucket.add(num);  
    }  
}

时间复杂度:
桶排序的时间复杂度取决于数据的分布、桶的数量以及桶内排序算法的效率。理想情况下,如果数据均匀分布,桶的数量合适,并且每个桶内使用线性时间的排序算法(如计数排序),那么桶排序的平均时间复杂度可以达到 𝑂(𝑛+𝑘),其中 𝑛是数据的数量,𝑘是桶的数量。
1.最好情况:O(n+k),每个桶的元素大致相同
2.最坏情况:O($n^2$),所有元素都被分到一个桶中
空间复杂度:
O(n+k)

基数排序

基数排序的本质也是桶排序,工作原理是按照数字的“基数”(例如个位、十位、百位等)来组织数据。这种排序方法特别适合用在数字长度固定的情况,例如电话号码、长整数等。
比如有一系列100到999的三位数要排序,排序步骤:
1. 按个位数排序:首先不管百位和十位,只按个位数字将成绩单分组放置。例如,所有个位是0的放一起,所有个位是1的放一起,以此类推。
2. 按十位数排序:完成个位排序后,再根据十位数的数字进行排序,这一步要保留个位的排序结果。这样,不仅十位数字相同的会聚在一起,个位的顺序也已经正确。
3. 按百位数排序:最后根据百位数进行排序。完成这一步后,所有的成绩单就完全按从小到大的顺序排列好了。

/**  
 * @param d:数字的最大位数,1位数是10,2位数是100....  
 */public static void sort(int[] nums, int d) {  
    int n = 1; // 从右往左的第n位,相同的放一个桶  
    int length = nums.length;  
    int[][] bucket = new int[10][length]; // 0-9 10个数字,最多length个数字放在同一个桶中  
    int[] order = new int[length]; // 记录各桶放了多少数字  
    while (n < d) {  
        // 分桶  
        for (int num : nums) {  
            int digit = (num / n) % 10; // 从右往左第n位  
            bucket[digit][order[digit]] = num; // 入桶  
            order[digit]++;  
        }  
        // 排序  
        int k = 0;  
        for (int i = 0; i < length; i++) {  
            if (order[i] != 0) {// 如果有该桶  
                for (int j = 0; j < order[i]; j++) {  
                    nums[k] = bucket[i][j];  
                    k++;  
                }  
            }  
            order[i] = 0;  
        }  
        n *= 10;  
    }  
}

时间复杂度:
O(nk)
空间复杂度:
O(n+k)

posted @ 2024-05-02 14:31  marigo  阅读(8)  评论(0编辑  收藏  举报