数据结构--排序(二)

1. 堆排序

1.1 堆排序原理

基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区
间的最大的数。

注意: 排升序要建大堆;排降序要建小堆。
在这里插入图片描述

1.2 堆排序实现

public static void heapSort(int[] arr) {
    // 先建堆
    createHeap(arr);
    // 取堆顶元素与最后一个元素交换,并删除最后一个元素
    // 再从 0 开始调整
    int heapSize = arr.length;
    for (int i = 0; i < arr.length; i++) {
        swap(arr, 0, heapSize - 1);
        // 删除最后一个元素
        heapSize--;
        // 从 0 开始调整
        shiftDown(arr, heapSize, 0);
    }
}
    public static void shiftDown(int[] arr, int size, int index) {
    int parent = index;
    int child = 2 * parent + 1;
    while (child < size) {
        // 先找出左右子树比较大的~
        if (child + 1 < size && arr[child + 1] > arr[child]) {
            child = child + 1;
        }
        // 再去比较 child 和 parent
        if (arr[parent] < arr[child]) {
            swap(arr, parent, child);
        } else {
            break;
        }
        parent = child;
        child = 2 * parent + 1;
    }
}
    public static void createHeap(int[] arr) {
    for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
        shiftDown(arr, arr.length, i);
    }
}


public static void swap(int[] arr, int x, int y) {
    int tmp = arr[x];
    arr[x] = arr[y];
    arr[y] = tmp;
}

注意:
1.堆排序是不稳定
2.时间复杂度:O(n * log(n))
3.空间复杂度:O(1)

2. 冒泡排序

2.1 冒泡排序原理

在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
在这里插入图片描述

2.2 冒泡排序实现

// 冒泡排序
public static void bubbleSort(int[] arr) {
    for (int bound = 0; bound < arr.length; bound++) {
        
        for (int cur = arr.length - 1; cur > bound; cur--) {
            if (arr[cur - 1] > arr[cur]) {
                swap(arr,cur - 1,cur);
            }
        }
    }
}


public static void swap(int[] arr, int x, int y) {
    int tmp = arr[x];
    arr[x] = arr[y];
    arr[y] = tmp;
}

注意:
1.冒泡排序是稳定的
2.时间复杂度:O(n^2)
3.空间复杂度:O(1)

3. 快速排序

3.1 快速排序原理

核心操作:partition

现在待排序数组中选取一个 “基准值” ,然后把这个数组整理成,左侧壁基准值小,右侧比基准值大(使用左右下标从两边往中间走实现)

在这里插入图片描述

3.2 快速排序实现(递归)

public static void quickSort(int[] arr) {
    // 创建一个辅助递归的方法.
    // 在这个方法的参数中, 明确指定针对哪个区间进行递归.
    // [0, length - 1]
    _quickSort(arr, 0, arr.length - 1);
}

public static void _quickSort(int[] arr, int left, int right) {
    if (left >= right) {
        // 如果当前的区间为空, 或者只有一个元素
        // 都不需要进行任何处理
        return;
    }
    // 现针对当前 [left, right] 区间进行 partition 操作
    // 方法的返回值, 表示整理完当前区间后, 基准值所在的位置.
    // 遍历过程中的 left 和 right 的重合位置
    int index = partition(arr, left, right);
    // 递归的对左侧区间进行快速排序
    _quickSort(arr, left, index - 1);
    // 递归的对右侧区间进行快速排序
    _quickSort(arr, index + 1, right);
}
public static int partition(int[] arr, int left, int right) {
    // 选取最右侧元素作为基准值.
    int v = arr[right];
    int l = left;
    int r = right;
    // 如果 l 和 r 重合, 说明遍历完成
    while (l < r) {
        // 先从左往右, 找一个比基准值大的数字.
        while (l < r && arr[l] <= v) {
            l++;
        }
        // 当循环结束的时候, l 就指向了比基准值大的元素
        // 再从右往左, 找一个比基准值小的数字
        while (l < r && arr[r] >= v) {
            r--;
        }
        swap(arr, l, r);
    }
    // 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
    swap(arr, l, right);
    // 最终方法返回基准值所在的位置(l 和 r 重合的位置)
    return l;
}
    public static void swap(int[] arr, int x, int y) {
    int tmp = arr[x];
    arr[x] = arr[y];
    arr[y] = tmp;
}

注意:
1.快速排序是不稳定的
2.时间复杂度:O(n * log(n))
3.空间复杂度:O(log(n))

快速排序的优化:
1.三数取中
2.当待处理区间比较小的时候,就不继续递归了,直接针对该区域进行插入排序;
3.当递归达到一定的深度,并且当前的待处理区间比较大,还可以使用堆排序。

3.3 快速排序实现(非递归)

public static void quickSortByLoop(int[] arr) {
    // 1. 先创建一个栈, 这个栈用来保存当前的每一个待处理区间
    Stack<Integer> stack = new Stack<>();
    // 2. 把根节点入栈, 整个数组对应的区间
    stack.push(0);
    stack.push(arr.length - 1);
    // 3. 循环取栈顶元素
    while (!stack.isEmpty()) {
        // 取的元素就是当前的待处理区间
        // 取的顺序正好和插入的顺序相反
        int right = stack.pop();
        int left = stack.pop();
        if (left >= right) {
            // 如果是空区间或者只有一个元素, 不需要排序
            continue;
        }
        // 调用 partition 方法整理当前区间
        int index = partition(arr, left, right);
        // 右侧区间: [index + 1, right]
        stack.push(index + 1);
        stack.push(right);
        // 左侧区间: [left, index - 1]
        stack.push(left);
        stack.push(index - 1);
    }
}

    public static int partition(int[] arr, int left, int right) {
        // 选取最右侧元素作为基准值.
        int v = arr[right];
        int l = left;
        int r = right;
        // 如果 l 和 r 重合, 说明遍历完成
        while (l < r) {
            // 先从左往右, 找一个比基准值大的数字.
            while (l < r && arr[l] <= v) {
                l++;
            }
            // 当循环结束的时候, l 就指向了比基准值大的元素
            // 再从右往左, 找一个比基准值小的数字
            while (l < r && arr[r] >= v) {
                r--;
            }
            swap(arr, l, r);
        }
        // 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
        swap(arr, l, right);
        // 最终方法返回基准值所在的位置(l 和 r 重合的位置)
        return l;
    }

4. 归并排序

4.1 归并排序原理

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

4.2 归并排序实现(递归)

public static void mergeSort(int[] arr) {
    _mergeSort(arr, 0, arr.length);
}

// 辅助递归的方法
public static void _mergeSort(int[] arr, int left, int right) {
    if (right - left <= 1) {
        // 判定当前区间是不是只有一个元素或者没有元素
        // 此时不需要进行排序
        return;
    }
    int mid = (left + right) / 2;
    // 先让 [left, mid) 区间变成有序
    _mergeSort(arr, left, mid);
    // 再让 [mid, right) 区间变成有序
    _mergeSort(arr, mid, right);
    // 合并两个有序区间
    merge(arr, left, mid, right);
}
// 归并排序中的关键操作, 就是归并两个有序数组.
// 使用该 merge 方法完成数组归并的过程
// 此处两个数组就通过参数的 left, mid, right 描述
// [left, mid) 左侧数组
// [mid, right) 右侧数组
public static void merge(int[] arr, int left, int mid, int right) {
    // 进行具体的归并操作
    // 需要创建一个临时的空间用来保存归并的结果
    // 临时空间得能保存下带归并的两个数组.
    // right - left 这么长
    if (left >= right) {
        // 空区间就直接忽略~~
        return;
    }
    int[] tmp = new int[right - left];
    int tmpIndex = 0; // 这个下标表示当前元素该放到临时空间的哪个位置上.
    int cur1 = left;
    int cur2 = mid;
    while (cur1 < mid && cur2 < right) {
        // 此处 最好写成 <= , 目的就是稳定性.
        // 由于 cur1 是在左侧区间, cur2 是在右侧区间.
        // 此时如果发现 cur1 和 cur2 的值相等,
        // 就希望左侧区间的 cur1 在最终结果中仍然是在左侧.
        // 于是就先把 cur1 对应的元素给先放到结果中.
        if (arr[cur1] <= arr[cur2]) {
            // 把 cur1 对应的元素插入到临时空间中
            tmp[tmpIndex] = arr[cur1];
            tmpIndex++;
            cur1++;
        } else {
            // 把 cur2 对应的元素插入到临时空间中
            tmp[tmpIndex] = arr[cur2];
            tmpIndex++;
            cur2++;
        }
    }
    // 循环结束之后, 需要把剩余的元素也都给拷贝到最终结果里.
    while (cur1 < mid) {
        tmp[tmpIndex] = arr[cur1];
        tmpIndex++;
        cur1++;
    }
    while (cur2 < right) {
        tmp[tmpIndex] = arr[cur2];
        tmpIndex++;
        cur2++;
    }
    // 还需要把 tmp 的结果再放回 arr 数组. (原地排序)
    // 把原始数组的 [left, right) 区间替换回排序后的结果
    for (int i = 0; i < tmp.length; i++) {
        arr[left + i] = tmp[i];
    }
}

注意:
1.归并排序是稳定的
2.时间复杂度:O(n * log(n))
3.空间复杂度:O(n)

4.3 归并排序(非递归)

public static void mergeSortByLoop(int[] arr) {
    // gap 用于限定分组.
    // gap 值的含义就是每个待归并数组的长度
    int gap = 1;
    for (; gap < arr.length; gap *= 2) {
        // 当前两个待归并的数组
        for (int i = 0; i < arr.length; i += 2*gap) {
            // 在这个循环中控制两个相邻的数组进行归并
            // [left, mid) 和 [mid, right) 就要进行归并
            int left = i;
            int mid = i + gap;
            if (mid >= arr.length) {
                mid = arr.length;
            }
            int right = i + 2 * gap;
            if (right >= arr.length) {
                right = arr.length;
            }
            merge(arr, left, mid, right);
        }
    }
}

// 归并排序中的关键操作, 就是归并两个有序数组.
// 使用该 merge 方法完成数组归并的过程
// 此处两个数组就通过参数的 left, mid, right 描述
// [left, mid) 左侧数组
// [mid, right) 右侧数组
public static void merge(int[] arr, int left, int mid, int right) {
    // 进行具体的归并操作
    // 需要创建一个临时的空间用来保存归并的结果
    // 临时空间得能保存下带归并的两个数组.
    // right - left 这么长
    if (left >= right) {
        // 空区间就直接忽略~~
        return;
    }
    int[] tmp = new int[right - left];
    int tmpIndex = 0; // 这个下标表示当前元素该放到临时空间的哪个位置上.
    int cur1 = left;
    int cur2 = mid;
    while (cur1 < mid && cur2 < right) {
        // 此处 最好写成 <= , 目的就是稳定性.
        // 由于 cur1 是在左侧区间, cur2 是在右侧区间.
        // 此时如果发现 cur1 和 cur2 的值相等,
        // 就希望左侧区间的 cur1 在最终结果中仍然是在左侧.
        // 于是就先把 cur1 对应的元素给先放到结果中.
        if (arr[cur1] <= arr[cur2]) {
            // 把 cur1 对应的元素插入到临时空间中
            tmp[tmpIndex] = arr[cur1];
            tmpIndex++;
            cur1++;
        } else {
            // 把 cur2 对应的元素插入到临时空间中
            tmp[tmpIndex] = arr[cur2];
            tmpIndex++;
            cur2++;
        }
    }
    // 循环结束之后, 需要把剩余的元素也都给拷贝到最终结果里.
    while (cur1 < mid) {
        tmp[tmpIndex] = arr[cur1];
        tmpIndex++;
        cur1++;
    }
    while (cur2 < right) {
        tmp[tmpIndex] = arr[cur2];
        tmpIndex++;
        cur2++;
    }
    // 还需要把 tmp 的结果再放回 arr 数组. (原地排序)
    // 把原始数组的 [left, right) 区间替换回排序后的结果
    for (int i = 0; i < tmp.length; i++) {
        arr[left + i] = tmp[i];
    }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值