当前位置: 首页 > article >正文

详解快排+归并排序+堆排序 附源码

  • 没发博客的日子,也在好好学习,这段时间,每天花四到五小时写算法,除了图论和dp都有了不少的认知,后面会减少算法的时间,主要就是复习之前刷分+刷完图论+并不断学习dp。
  • 这篇文章,是因为在复习链表题的时候发现排序题不会写,重新复习了几个较难的排序。

三、排序

3.1快速排序总结 (升序排序)
  1. 选择基准元素

    • 在快速排序中,选择一个基准元素(pivot)。常见的做法是选取最右边的元素(nums[right])作为基准。
  2. 分区过程

    • 快速排序通过 分区 操作将数组分为两部分:一部分包含所有小于基准元素的值,另一部分包含所有大于基准元素的值。
    • 在此过程中,两个指针 l和r会在数组的两端移动:
      • l 从左边开始,寻找第一个大于基准元素的元素。
      • r 从右边开始,寻找第一个小于基准元素的元素。
    • 如果 l 小于 r,则交换这两个元素,直到 lr 相遇。
  3. 基准元素交换

    • lr 相遇后,基准元素会交换到 lr 的位置,确保基准元素左边的元素都小于基准元素,右边的元素都大于基准元素。
    • 此时,返回基准元素的位置 l(或者 r,它们此时相同),将其放置在正确的排序位置。
  4. 递归排序

    • 基准元素确定位置后,递归地对左右两部分进行排序。
  • 代码:
class Solution {
     public int[] sortArray(int[] nums) {
         quickSort(nums, 0, nums.length - 1);
         return nums;
     }
 
     private void swap(int[] nums, int a, int b) {
         int temp = nums[a];
         nums[a] = nums[b];
         nums[b] = temp;
     }
 
     private int partition(int[] nums, int left, int right) {
         // 选择基准元素,通常选择最右边的元素
         int pivot = nums[right];
         int l = left;  // l 指向左边部分的开始
         int r = right - 1;  // r 指向右边部分的开始
 
         while (l <= r) {
             // 寻找一个小于基准元素的元素
             while (l <= r && nums[l] < pivot) {
                 l++;
             }
             // 寻找一个大于基准元素的元素
             while (l <= r && nums[r] >= pivot) {
                 r--;
             }
             if (l < r) {
                 swap(nums, l, r);  // 交换 l 和 r 的元素
             }
         }
 
         // 将基准元素放到正确位置
         swap(nums, right, l);  // 基准元素放置到 l 位置
         return l;  // 返回基准元素的位置
     }
 
     public void quickSort(int[] nums, int left, int right) {
         if (left >= right) {
             return;
         }
         int pivot = partition(nums, left, right);  // 获取基准元素的最终位置
         quickSort(nums, left, pivot - 1);  // 排序左半部分
         quickSort(nums, pivot + 1, right);  // 排序右半部分
     }
 }
  • 基准和谁交换,看的是,哪个指针指向的大小和基准原来位置的关系。例如选择升序排,左基准,那么就是基准和右指针换,因为右指针指向的是比基准小的数。例如升序,右基准,那么就是左指针和基准换,因为指针指向的是比基准大的数。
3.2基准优化
  • 选择左中间右三个节点,选择中位数为基准,能大程度少优化时间复杂度

    class Solution {
        public int[] sortArray(int[] nums) {
            quickSort(nums, 0, nums.length - 1);
            return nums;
        }
    
        public void quickSort(int[] nums, int left, int right) {
            if (left >= right) {
                return;
            }
            int pivot = partition(nums, left, right);
            quickSort(nums, left, pivot - 1);
            quickSort(nums, pivot + 1, right);
        }
    
        public int medianThree(int[] nums, int left, int mid, int right) {
            int l = nums[left];
            int m = nums[mid];
            int r = nums[right];
            if ((l <= m && m <= r) || (r <= m && m <= l)) {
                return mid;
            }
            else if ((m <= l && l <= r) || (r <= l && l <= m)) {
                return left;
            }
            return right;
        }
    
        public void swap(int[] nums, int left, int right) {
            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
        }
    
        public int partition(int[] nums, int left, int right) {
            int pivotIndex = medianThree(nums, left, (left + right) / 2, right);
            swap(nums, pivotIndex, left);
            int pivot = nums[left];
            int l = left + 1; int r = right;
            while (l <= r) {
                while (l <= r && nums[l] <= pivot) {
                    l++;
                }
                while (l <= r && nums[r] >= pivot) {
                    r--;
                }
                if (l < r) {
                    swap(nums, r, l);
                }
            }
            swap(nums, r, left);
            return r;
        }
    }
    
3.3尾递归优化
  • 当按顺序排列,时间复杂度和空间复杂度最高,为了防止栈溢出,先对数组元素较小的子数组进行排序

    class Solution {
        public int[] sortArray(int[] nums) {
            quickSort(nums, 0, nums.length - 1);
            return nums;
        }
    
        public void quickSort(int[] nums, int left, int right) {
            while (left < right) {
                int pivot = partition(nums, left, right);
                if (pivot - left < right - pivot) {
                    quickSort(nums, left, pivot - 1);
                    left = pivot + 1;
                } else {
                    quickSort(nums, pivot + 1, right); // 递归排序右子数组
                    right = pivot - 1;
                }
            }
        }
    
        public int medianThree(int[] nums, int left, int mid, int right) {
            int l = nums[left];
            int m = nums[mid];
            int r = nums[right];
            if ((l <= m && m <= r) || (r <= m && m <= l)) {
                return mid;
            }
            else if ((m <= l && l <= r) || (r <= l && l <= m)) {
                return left;
            }
            return right;
        }
    
        public void swap(int[] nums, int left, int right) {
            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
        }
    
        public int partition(int[] nums, int left, int right) {
            int pivotIndex = medianThree(nums, left, (left + right) / 2, right);
            swap(nums, pivotIndex, left);
            int pivot = nums[left];
            int l = left + 1; int r = right;
            while (l <= r) {
                while (l <= r && nums[l] <= pivot) {
                    l++;
                }
                while (l <= r && nums[r] >= pivot) {
                    r--;
                }
                if (l < r) {
                    swap(nums, r, l);
                }
            }
            swap(nums, r, left);
            return r;
        }
    }
    

    如果是普通递归,那么一下就会占满,栈空间

    我们可以在每轮哨兵排序完成后,比较两个子数组的长度,仅对较短的子数组进行递归。由于较短子数组的长度不会超过 n/2 ,因此这种方法能确保递归深度不超过 log⁡n ,从而将最差空间复杂度优化至 O(log⁡n) 。

3.2归并排序
3.2.1自顶向下(递归排序)

数组排序:先递归把数组切分到只剩一个元素(注意切分的时候每个数组元素的切分边界,虽然不影响结果,但我推荐使用整除2,因为这样易于理解)

假设我们有一个数组 [3, 5, 8, 4, 7, 2, 1],它会按照如下步骤进行分解和合并:

  1. 初始数组:[3, 5, 8, 4, 7, 2, 1]
    • 拆分为两部分:[3, 5, 8, 4][7, 2, 1]
  2. 继续拆分:
    • [3, 5, 8, 4] 拆分为 [3, 5][8, 4]
    • [7, 2, 1] 拆分为 [7][2, 1]
  3. 继续拆分直到每部分都为单个元素:
    • [3, 5, 8, 4] 被拆分成 [3][5], [8], [4]
    • [7, 2, 1] 被拆分成 [7], [2], [1]
  4. 然后开始合并,合并的子数组大小不一定相等:
    • [3][5] 合并为 [3, 5]
    • [8][4] 合并为 [8, 4]
    • [7][2] 合并为 [7, 2]
    • [3, 5][8, 4] 合并为 [3, 5, 8, 4]
    • [1][7, 2] 合并为 [1, 2, 7]
  5. 最后合并两大部分:
    • 合并为 [1, 2, 3, 4, 5, 7, 8]
class Solution {
    public int[] sortArray(int[] nums) {
        int n = nums.length;
        int[] temp = new int[n];
        mergeSort(nums, 0, nums.length - 1, temp);
        return nums;
    }

    public void mergeSort(int[] nums, int left, int right, int[] temp) {
        if (left >= right) {
            return;
        }
        // 整除2,就是偶数偏右,奇数中间,这里需要对半分所有是除2,这样好看些,但是其实并不影响结果
        int mid = (left + right) / 2;
        mergeSort(nums, left, mid, temp);
        mergeSort(nums, mid + 1, right, temp);
        merge(nums, left, mid, right, temp);
    }

    public void merge(int[] nums, int left, int mid, int right, int[] temp) {
        int i = left, m = mid + 1, j = right, k = 0;
        // 对半分,左边要包括mid,因为上面是/2
        while (i <= mid && m <= right) {
            if (nums[i] <= nums[m]) {
                temp[k] = nums[i];
                i++;
            } else {
                temp[k] = nums[m];
                m++;
            }
            k++;
        }
        while (i <= mid) {
            temp[k++] = nums[i++];
        }
        while (m <= right) {
            temp[k++] = nums[m++];
        }
        k--; // k 指向最后一个有效元素
        while (k >= 0) {
            nums[right--] = temp[k--];  // 使用 right--,逐步填充 nums
        }
    }
}
  • 链表的排序

    class Solution {
        private ListNode middleNode(ListNode head) {
            ListNode slow = head;
            ListNode fast = head;
            while (fast.next != null && fast.next.next != null) {
                slow = slow.next;
                fast = fast.next.next;
            }
            ListNode mid = slow.next;
            slow.next = null;  // 断开mid的前一个节点和mid的连接
            return mid;
        }
    
        private ListNode merge(ListNode list1, ListNode list2) {
            ListNode dummy = new ListNode();
            ListNode cur = dummy;
            while (list1 != null && list2 != null) {
                if (list1.val > list2.val) {
                    cur.next = list2;
                    list2 = list2.next;
                } else {
                    cur.next = list1;
                    list1 = list1.next;
                }
                cur = cur.next;
            }
            cur.next = (list1 != null ? list1 : list2);
            return dummy.next;
        }
    
        public ListNode sortList(ListNode head) {
            if (head == null || head.next == null) {
                return head;
            }
            ListNode head2 = middleNode(head);
            head = sortList(head);
            head2 = sortList(head2);
    
            return merge(head, head2);
        }
    }
    
3.2.2自顶向上(非递归排序)
  • 合并过程和递归是一样的,只是在分隔的时候不一样,记住,如果数组元素有两个,加上2,就变成三个,记得减一。

    思路就是先for循环递增一共step,表示合并区间不端变大,然后size标识合并数组区间,mid为什么去min?

    是为了防止最后不满left + step - 1,防止越界访问。right也是同理,防止最后的区间不满足left + step*2 - 1然后非法访问出错

    class Solution {
        public int[] sortArray(int[] nums) {
            int n = nums.length;
            int[] temp = new int[n];
            mergeSort(nums, temp);
            return nums;
        }
    
        public void mergeSort(int[] nums, int[] temp) {
            int n = nums.length;
            for (int step = 1; step < n; step *= 2) {
                // 分块合并的过程
                for (int left = 0; left < n; left += step * 2) {
                    int mid = Math.min(left + step - 1, n - 1); // 中间位置
                    int right = Math.min(left + 2 * step - 1, n - 1); // 右边界
                    merge(nums, left, mid, right, temp);
                }
            }
        }
    
        public void merge(int[] nums, int left, int mid, int right, int[] temp) {
            int i = left, m = mid + 1, j = right, k = 0;
            // 对半分,左边要包括mid,因为上面是/2
            while (i <= mid && m <= right) {
                if (nums[i] <= nums[m]) {
                    temp[k] = nums[i];
                    i++;
                } else {
                    temp[k] = nums[m];
                    m++;
                }
                k++;
            }
            while (i <= mid) {
                temp[k++] = nums[i++];
            }
            while (m <= right) {
                temp[k++] = nums[m++];
            }
            k--; // k 指向最后一个有效元素
            while (k >= 0) {
                nums[right--] = temp[k--];  // 使用 right--,逐步填充 nums
            }
        }
    }
    
  • 链表排序

    class Solution {
        private ListNode split(ListNode head, int size) {
            for (int i = 0; i < size - 1 && head != null; i++) {
                head = head.next;
            }
            if (head == null || head.next == null) {
                return null;
            }
            ListNode temp = head.next;
            head.next = null;
            return temp;
        }
    
        private ListNode[] merge(ListNode head1, ListNode head2) {
            ListNode dummy = new ListNode(0); 
            ListNode cur = dummy;
            while (head1 != null && head2 != null) {
                if (head1.val > head2.val) {
                    cur.next = head2;
                    head2 = head2.next;
                } else {
                    cur.next = head1;
                    head1 = head1.next;
                }
                cur = cur.next;
            }
            cur.next = head1 != null ? head1 : head2;
            while (cur.next != null) {
                cur = cur.next;
            }
            return new ListNode[]{dummy.next, cur};
        }
    
        private int getLength(ListNode head) {
            int count = 0;
            while (head != null) {
                head = head.next;
                count++;
            }
            return count;
        } 
    
        public ListNode sortList(ListNode head) {
            if (head == null) return null;
            
            // 先计算一次长度
            int length = getLength(head);
            ListNode dummy = new ListNode(0, head);
            
            for (int step = 1; step < length; step *= 2) {
                ListNode cur = dummy.next;
                ListNode newTail = dummy;
                while (cur != null) {
                    ListNode head1 = cur;
                    ListNode head2 = split(head1, step);
                    cur = split(head2, step);
                    ListNode[] merged = merge(head1, head2);
                    newTail.next = merged[0];
                    newTail = merged[1];
                }
            }
            return dummy.next;
        }
    }
    

3.3堆排序

  • 流程

    1. 输入数组并建立大顶堆。完成后,最大元素位于堆顶。
    2. 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减 1 ,已排序元素数量加 1 。
    3. 从堆顶元素开始,从顶到底执行堆化操作(sift down)。完成堆化后,堆的性质得到修复。
    4. 循环执行第 2. 步和第 3. 步。循环 n−1 轮后,即可完成数组排序。
  • 代码

    class Solution {
        public int[] sortArray(int[] nums) {
            return heapSort(nums);
        }
    
        public int[] heapSort(int[] nums) {
            int n = nums.length;
            for (int i = n / 2 - 1; i >= 0; i--) {
                shiftdown(nums, n, i);
            }
            for (int i = n - 1; i > 0; i--) {
                int tmp = nums[0];
                nums[0] = nums[i];
                nums[i] = tmp;
                shiftdown(nums, i, 0);
            }
            return nums;
        }
    
        private void shiftdown(int[] nums, int n, int i) {
            while (true) {
                int l = i * 2 + 1;
                int r = i * 2 + 2;
                int ma = i;
                if (l < n && nums[l] > nums[ma])
                ma = l;
                if (r < n && nums[r] > nums[ma])
                    ma = r;
                // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
                if (ma == i)
                    break;
                int temp = nums[ma];
                nums[ma] = nums[i];
                nums[i] = temp;
                i = ma;
            }
        }
    
    }
    

    首先根据堆的性质,shiftdown除叶子节点所有节点,只需要0(n)复杂度,然后进行原地交换排序实现


http://www.kler.cn/a/447006.html

相关文章:

  • Windows中运行Linux(WSL)
  • python rabbitmq实现简单/持久/广播/组播/topic/rpc消息异步发送可配置Django
  • 回归预测 | MATLAB实现CNN-BiGRU-Attention卷积神经网络结合双向门控循环单元融合注意力机制多输入单输出回归预测
  • Git实用指南(精简版)
  • Elasticsearch-DSL高级查询操作
  • linux-----进程及基本操作
  • thinking claude从入门到精通
  • 前端中的拖拽知识
  • 前端利用JS实现自定义表格滚动效果
  • 【C++】角谷猜想问题分析与解答
  • 基于Java Web的“使用Ajax实现无刷新实时显示公告信息”实验
  • Spring实例化的基本流程和Bean处理器
  • LeetCode 2545.根据第 K 场考试的分数排序:考察编程语言的排序
  • 现代 CSS 布局与响应式设计实战指南
  • asp.net多媒体教室管理系统VS开发sqlserver数据库web结构c#编程计算机网页项目
  • 使用Mac自带共享实现远程操作
  • TANGO与LabVIEW控制系统集成
  • [ESP]从零开始的Arduino IDE安装与ESP环境配置教程
  • HBase、Hive、Redis 和 MongoDB的对比
  • C语言的函数指针
  • linux-----文件命令
  • Latex 转换为 Word(使用GrindEQ )(英文转中文,毕业论文)
  • AdminJS - 集成 MySQL 的现代化管理面板开发指南
  • CSS3 实现火焰-小火苗效果
  • linux中大内核锁、互斥锁、信号量、完成变量、自旋锁区别
  • 【AIGC-ChatGPT进阶提示词-《动图生成》】怪物工厂:融合想象力与创造力的奇幻世界