排序(快速排序(包含栈实现),归并排序(包含非递归版本),堆排序,插入排序,希尔排序,选择排序)

讲排序前,先来了解排序中一个不重要但又很重要的点:稳定性

如果排序中存在两个值相同的元素,排序前后,保证这两个值的相对位置不变,则说明此排序是稳定的
例:9 5 2(a) 8 7 2(b) 4
即排序后2(a)依旧在2(b)前面,则此排序算法为稳定排序

友情提示:若需要排序的动画演示,十分推荐B站UP主:"有个知识",个人认为其动画演示十分清晰易懂

大致链接:【两分钟认识插入排序】https://www.bilibili.com/video/BV1W841197rx?vd_source=9f4130957b41e56dead3e5dea7cdb26c

现在开始介绍各类排序方法

一,选择排序

public class SelectSort {
    /**
     * 选择排序算法实现
     * 基本思想:将数组分为已排序区间(初始为0)和未排序区间,
     * 通过比较的方式将未排序区间中的最小值(或最大值)放到已排序区间中去
     * 当bound达到length-1时,整个数组就排序完毕了
     * 
     * 时间复杂度:O(n²)
     * 空间复杂度:O(1)
     * 不稳定排序(因为交换可能改变相等元素的相对位置)
     * 
     */
    private static void selectSort(int[] arr) {
        // 外层循环控制已排序区间的边界
        // 当bound达到arr.length-1时,排序完成(最后一个元素无需比较)
        for (int bound = 0; bound < arr.length - 1; bound++) {
            // 内层循环遍历未排序区间
            for (int cur = bound + 1; cur < arr.length; cur++) {
                // 如果发现未排序区间中有比当前bound位置更小的元素
                if (arr[cur] < arr[bound]) {
                    // 交换这两个元素的位置
                    int temp = arr[cur];
                    arr[cur] = arr[bound];
                    arr[bound] = temp;
                }
            }
            // 每轮循环结束后,bound位置的元素就是未排序区间中的最小值
            // 已排序区间扩大1
        }
    }
}

二,插入排序

public class InsertSort {
    /**
     * 插入排序算法实现
     * 基本思想:将数组分为有序区间和无序区间,每次从无序区间取出第一个元素,
     * 在有序区间中找到合适的位置插入
     * 
     * 时间复杂度:
     *   - 最好情况(已有序):O(n)
     *   - 最坏情况(逆序):O(n²)
     *   - 平均:O(n²)
     * 空间复杂度:O(1)
     * 稳定排序(相等元素不会改变相对位置)
     *
     */
    private static void insertSort(int[] arr) {
        // bound表示有序区间和无序区间的分界
        // 初始时有序区间只有第一个元素(arr[0]),所以bound从1开始
        for (int bound = 1; bound < arr.length; bound++) {
            // 保存当前待插入的元素值
            int value = arr[bound];
            // cur用于在有序区间中从后往前查找插入位置
            int cur = bound - 1;
            
            // 在有序区间中寻找合适的插入位置
            for (; cur >= 0; cur -= 1) {
                // 如果当前元素比待插入元素大,则向后移动
                if (arr[cur] > value) {
                    arr[cur + 1] = arr[cur];
                } else {
                    // 找到合适位置(前一个元素<=value),跳出循环
                    break;
                }
            }
            // 将待插入元素放到正确位置
            arr[cur + 1] = value;
        }
    }
}

三,希尔排序

        注意:希尔排序属于插入排序的变种或者说优化版

倘若只是为了方便记忆,可以直接把插入排序中的bound - 1,cur -= 1,cur + 1中的1全部改为gap即可

package Sort;

import java.util.Arrays;

public class ShellSort {
    /**
     * 希尔排序(缩小增量排序)
     * 基本思想:将数组按照增量gap分组,对每组进行插入排序,
     * 然后逐步缩小gap直到1,最后进行一次完整的插入排序
     * 
     * 时间复杂度:取决于增量序列,最好可达O(n log²n)
     * 空间复杂度:O(1)
     * 不稳定排序(分组插入可能改变相等元素的相对位置)
     * 
     */
    private static void shellSort(int[] arr) {
        // 初始化gap为数组长度,使用希尔增量序列(gap = gap/2)
        int gap = arr.length;
        
        // 当gap >= 1时继续排序
        while (gap >= 1) {
            // 对当前gap值进行分组插入排序
            insertSortGap(arr, gap);
            // 缩小gap值
            gap /= 2;
        }
    }

    /**
     * 按指定gap值进行分组插入排序
     * 
     */
    private static void insertSortGap(int[] arr, int gap) {
        // bound从gap开始,表示每个分组的第二个元素(第一个元素视为已排序)
        for (int bound = gap; bound < arr.length; bound++) {
            // 保存当前待插入的元素值
            int value = arr[bound];
            // cur指向同组前一个元素
            int cur = bound - gap;
            
            // 在组内向前查找插入位置
            for (; cur >= 0; cur -= gap) {
                if (arr[cur] > value) {
                    // 前移较大的元素
                    arr[cur + gap] = arr[cur];
                } else {
                    // 找到合适位置,停止查找
                    break;
                }
            }
            // 插入元素到正确位置
            arr[cur + gap] = value;
        }
    }
}

四,堆排序

/**
 * 堆排序算法实现
 * 基本思想:利用堆这种数据结构进行排序,分为建堆和排序两个阶段
 * 
 * 时间复杂度:O(n log n)
 * 空间复杂度:O(1)
 * 不稳定排序(堆调整过程可能改变相等元素的相对位置)
 */
public static void heapSort(int[] arr) {
    // 1. 构建初始大顶堆(将数组调整为大顶堆结构)
    createHeap(arr);
    
    // 2. 排序阶段:循环将堆顶元素与待排序区末尾元素交换,并调整堆
    // [0, bound]为待排序区间
    int bound = arr.length - 1;  // bound表示待排序区间的末尾边界
    
    // 共需要n-1次交换和调整操作
    for (int i = 0; i < arr.length - 1; i++) {
        // 将堆顶元素(当前最大值)与待排序区末尾交换
        swap(arr, 0, bound);
        
        // 对新的堆顶元素进行下沉操作,重新调整堆结构
        // 调整范围是[0, bound),bound位置已经是当前最大值
        shiftDown(arr, bound, 0);
        
        // 缩小待排序区间
        bound--;
    }
}

/**
 * 构建大顶堆
 * @param arr 待构建的数组
 */
private static void createHeap(int[] arr) {
    // 最后一个叶子节点的下标
    int lastLeaf = arr.length - 1;
    // 最后一个非叶子节点的下标(完全二叉树性质)
    int lastNonLeaf = (lastLeaf - 1) / 2;
    
    // 从最后一个非叶子节点开始,向前依次进行下沉操作
    for (int i = lastNonLeaf; i >= 0; i--) {
        shiftDown(arr, arr.length, i);
    }
}

/**
 * 下沉操作(调整堆结构)
 */
private static void shiftDown(int[] arr, int length, int index) {
//length为当前堆的有效长度,index为需要下沉的节点下标
    int parent = index;       // 当前父节点
    int child = 2 * parent + 1; // 左孩子节点
    
    // 当孩子节点在堆范围内时循环
    while (child < length) {
        // 如果右孩子存在且比左孩子大,则选择右孩子
        if (child + 1 < length && arr[child + 1] > arr[child]) {
            child = child + 1;
        }
        
        // 如果孩子节点大于父节点,则交换
        if (arr[child] > arr[parent]) {
            swap(arr, child, parent);
            
            // 继续向下检查
            parent = child;
            child = 2 * parent + 1;
        } else {
            // 如果父节点已经比孩子大,则调整结束
            break;
        }
    }
}

五,快速排序

/**
 * 快速排序算法实现(递归版本)
 * 基本思想:通过一趟排序将待排记录分隔成独立的两部分,
 * 其中一部分记录的关键字均比另一部分的关键字小,
 * 然后分别对这两部分记录继续进行排序,以达到整个序列有序
 * 
 * 时间复杂度:
 *   - 平均:O(n log n)
 *   - 最坏:O(n²)(当数组已经有序时)
 * 空间复杂度:O(log n)(递归调用栈)
 * 不稳定排序(交换操作可能改变相等元素的相对位置)
 */
private static void quickSort(int[] arr) {
    quickSort(arr, 0, arr.length - 1);
}

/**
 * 快速排序递归辅助方法
 * @param arr 待排序数组
 * @param left 当前处理区间的左边界(包含)
 * @param right 当前处理区间的右边界(包含)
 */
private static void quickSort(int[] arr, int left, int right) {
    // 递归终止条件:区间长度小于等于1
    if (left >= right) {
        return;
    }
    
    // 分区操作,返回基准值最终位置
    int index = partition(arr, left, right);
    
    // 递归处理左半部分
    quickSort(arr, left, index - 1);
    // 递归处理右半部分
    quickSort(arr, index + 1, right);
}

/**
 * 分区操作(挖坑填数法)
 * @param arr 待排序数组
 * @param left 区间左边界
 * @param right 区间右边界(基准值位置)
 * @return 基准值的最终位置
 */
private static int partition(int[] arr, int left, int right) {
    // 选择最右侧元素作为基准值
    int pivot = arr[right];
    int l = left;   // 左指针
    int r = right;  // 右指针
    
    while (l < r) {
        // 从左往右找第一个大于基准值的元素
        while (l < r && arr[l] <= pivot) {
            l++;
        }
        
        // 从右往左找第一个小于基准值的元素
        while (l < r && arr[r] >= pivot) {
            r--;
        }
        
        // 交换这两个元素
        swap(arr, l, r);
    }
    
    // 将基准值放到最终位置(l和r相遇的位置)
    swap(arr, l, right);
    
    // 返回基准值的最终位置
    return l;
}


特别:非递归,用栈实现的快排

/**
 * 区间范围类(用于非递归实现)
 */
static class Range {
    public int left;
    public int right;

    public Range(int left, int right) {
        this.left = left;
        this.right = right;
    }
}

/**
 * 快速排序非递归实现(使用栈模拟递归)
 * @param arr 待排序数组
 */
public static void quickSortByLoop(int[] arr) {
    Stack<Range> stack = new Stack<>();
    // 初始压入整个数组范围
    stack.push(new Range(0, arr.length - 1));
    
    while (!stack.isEmpty()) {
        Range range = stack.pop();
        
        // 区间长度小于等于1时跳过
        if (range.left >= range.right) {
            continue;
        }
        
        // 分区操作
        int index = partition(arr, range.left, range.right);
        
        // 将右半区间先压栈(保证左半区间先处理)
        stack.push(new Range(index + 1, range.right));
        // 将左半区间压栈
        stack.push(new Range(range.left, index - 1));
    }
}

六,归并排序

/**
 * 归并排序(递归版本)
 * 基本思想:采用分治法,将数组分成两半分别排序,然后将排序好的两部分合并
 * 
 * 时间复杂度:O(n log n)
 * 空间复杂度:O(n)(需要额外临时数组)
 * 稳定排序(合并时相等元素不会改变相对位置)
 */
private static void mergeSort(int[] arr) {
    mergeSort(arr, 0, arr.length - 1);
}

/**
 * 递归归并排序辅助方法
 * @param arr 待排序数组
 * @param left 当前处理区间的左边界(包含)
 * @param right 当前处理区间的右边界(包含)
 */
private static void mergeSort(int[] arr, int left, int right) {
    // 递归终止条件:区间长度小于等于1
    if (left >= right) {
        return;
    }
    
    // 计算中间位置
    int mid = (left + right) / 2;
    
    // 递归排序左半部分
    mergeSort(arr, left, mid);
    // 递归排序右半部分
    mergeSort(arr, mid + 1, right);
    
    // 合并两个有序区间
    merge(arr, left, mid, right);
}

/**
 * 合并两个有序子数组
 * @param arr 原始数组
 * @param left 左区间起始位置
 * @param mid 左区间结束位置(右区间起始位置为mid+1)
 * @param right 右区间结束位置
 */
private static void merge(int[] arr, int left, int mid, int right) {
    // 1. 创建临时数组保存合并结果
    int[] result = new int[right - left + 1];
    int resultSize = 0; // 记录已插入元素个数
    
    // 2. 设置两个指针分别指向两个区间的起始位置
    int cur1 = left;    // 左区间指针
    int cur2 = mid + 1; // 右区间指针
    
    // 3. 比较两个区间的元素,按顺序放入临时数组
    while (cur1 <= mid && cur2 <= right) {
        if (arr[cur1] <= arr[cur2]) {
            result[resultSize++] = arr[cur1++];
        } else {
            result[resultSize++] = arr[cur2++];
        }
    }
    
    // 4. 处理剩余元素(左区间或右区间可能还有剩余元素)
    while (cur1 <= mid) {
        result[resultSize++] = arr[cur1++];
    }
    while (cur2 <= right) {
        result[resultSize++] = arr[cur2++];
    }
    
    // 5. 将合并结果拷贝回原数组
    System.arraycopy(result, 0, arr, left, result.length);
//  相当于for(int i = 0;i< result.length;i++){
//            arr[left+i] = result[i];
//        }
}

特别:非递归版本的归并排序

/**
 * 归并排序(非递归版本)
 * 基本思想:自底向上,先两两合并,再四四合并,直到整个数组有序
 * 
 * @param arr 待排序数组
 */
private static void mergeSortByLoop(int[] arr) {
    // 外层循环控制合并的数组大小,从1开始,每次翻倍
    for (int size = 1; size < arr.length; size *= 2) {
        // 内层循环处理每个合并对
        for (int i = 0; i < arr.length; i += size * 2) {
            // 计算当前合并的两个子数组边界
            int left = i;
            int mid = i + size - 1;
            
            // 处理边界情况,防止数组越界
            if (mid >= arr.length - 1) {
                mid = arr.length - 1;
            }
            
            int right = i + size * 2 - 1;
            if (right >= arr.length - 1) {
                right = arr.length - 1;
            }
            
            // 合并这两个子数组
            merge(arr, left, mid, right);//merge方法重用上面的就行
        }
    }
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值