目标

  1. 使用分治法解决快速排序问题
  2. 使用分治法解决棋盘覆盖问题

原理

一、快排原理

排序算法在工作中最常用,也是学习很多其他算法的前置知识,例如在运用二分查找算法之前,我们通常需要保证数据是有序的,如果数据无序,我们还要对数据进行排序。在程序员的面试中,排序算法也经常会配合其他算法进行综合考察,比如经典的 2 − s u m 2-sum 2−sum 问题,就需要在一组无序数字中,找到两个数字相加之和等于目标值,其中一种做法就是先对数据进行排序,然后采用头尾指针扫描法来解决。所以,想要学好数据结构与算法,掌握排序算法是第一步。

题目:假设给你一组和排序算法有关的题目:假设给你一组无序的数字,让你找到其中排名第k位的数字,你会怎么做?

解决办法:利用选择排序算法,对这组数字进行排序。选择排序的过程如下图所示:

具体来说,就是用选择排序算法将数组中的元素,分成已排序区和待排序区。然后,每一轮从待排序区中选择一个最小值元素,放到已排序区的末尾,也就是待排序区的头部。这样,每一次的选择操作,都会使已排序区的长度增加一位,那经过 n − 1 n-1 n−1 轮选择操作以后,整个数组就是一个有序数组了。

因此,如果想找到排名第 k k k 位的元素,只需要做 k k k 次选择操作就可以了。这种做法的时间复杂度是 O ( k ∗ n ) O(k*n) O(k∗n),当 k k k 值接近 n / 2 n/2 n/2 的时候,时间复杂度就是 O ( n 2 2 ) O(\frac{n^2}{2} ) O(2n2​)。

为了更快地解决这个问题,可以使用一种经典的排序算法,快速排序( Q u i c k s o r t Quicksort Quicksort)算法。

快速排序是一种优秀的排序算法。这一点毫无疑问,可并不代表这种排序算法就是无敌的,它也有自己的问题。所有在工程实现中,我们往往使用的都是混合排序算法,例如 C + + C++ C++ S T L STL STL 的 s o r t sort sort,使用的就是“快速排序+插入排序+堆排序”的方式。所以,在面对算法学习的过程中,永远不要功利心太重,因为你永远不会知道,你抛弃掉的是什么样的伟大思想。

除了快速排序算法以外,还有一种由快速排序算法所延伸出来的,叫做快速选择( Q u i c k Quick Quick S e l e c t i o n Selection Selection)的算法。快速选择算法将是解决今天这个问题的终极利器。

今天这个问题不止一种解决方案,虽说快速选择算法是我们最后要讲的方法,可实际上,学习了快速排序以后,你也可以既快又好地解决这个问题,只不过使用快速选择会比快速排序更好而已。

理解快速排序算法的核心思想

基础的快速排序算法思想很简单,核心就是一句话:找到基准值的位置。

具体的过程其实和把大象装进冰箱这个问题一样,都可以分成三步:第一步,选择一个值作为基准值;第二步,找到基准值的位置,并将小于基准值的元素放在基准值的前面,大于基准值的元素放在基准值的后面;第三步,对基准值的左右两侧递归地进行这个过程。这么说还是很抽象,下面将分步来讲解。

第一步,选择一个值作为基准值,最简单的选择方法,一定是选择待排序区间的头部元素作为基准值。如下图所示,选择 8 8 8 作为本轮排序的基准值。

确定了本轮操作的基准值以后,快速排序的第二步,就是 将小于基准值的元素放在基准值的前面,将大于基准值的元素放在基准值的后面。 这一步通常被叫做 p a r t i t i o n partition partition 操作,中文直译过来就是分割操作,也就是用基准值将原数组分割成前后两部分。

理解了 p a r t i t i o n partition partition 操作的目的以后,再来讨论它具体是怎么做的。 p a r t i t i o n partition partition 操作简单来说,就是空出一个位置,反复地前后调换元素。这该怎么理解呢?首先,你要理解一点,当选择了基准值以后,原先基准值的位置就相当于被空出来了,也就是说数组的第一位是空着的。

如果第一位是空的,那剩下的事儿就好办了。我们借助这个空位,将后面小于基准值的元素放到前面的空位上,这样后面就空出一位了。然后,我们再将前面小于基准值的元素放到后面这个空位上。就这样交替进行,直到空位前面的值都小于基准值,空位后面的值都大于基准值。过程如下图所示:

快速排序的第三步,是对基准值的左右两侧,递归地进行第一步和第二步。 也就是说,我们要分别对 6 、 3 、 7 、 2 6、3、7、2 6、3、7、2 和 10 、 9 、 12 10、9、12 10、9、12 这两部分,再做选择基准值,找基准值位置和递归这三步。由于每次 p a r t i t i o n partition partition 操作中。我们都会确定一个值,也就是基准值的正确位置,所以,经过有限次递归操作以后,整个数组也就变成了一个有序数组。

当然,像上面这样的描述是不严谨的,但它确实是正确的。而且严谨的证明过程太过复杂,可以试着用数学归纳法和递归程序之间的关系来证明快速排序算法的正确性。

分析快速排序的时间复杂度

看完了快速排序的算法过程。再来分析一下快速排序的时间复杂度。可以借助二叉树的结构。来理解和分析快速排序算法的时间复杂度。

在讲快速排序算法过程的时候,其中最关键的步骤是理解 p a r t i t i o n partition partition 操作。因此,分析快速排序的时间复杂度,也要先来分析 p a r t i t i o n partition partition 操作这一步的时间复杂度。

在 p a r t i t i o n partition partition 操作的过程中,头指针会循环扫描到基准值最后放置的位置,尾指针也会扫描到最后基准值放置的位置,这样,头尾指针扫描加在一起,其实相当于扫描了整个待排序数组的区域。因此,我们就能得到单次 p a r t i t i o n partition partition 操作的时间复杂度为 O ( n ) O(n) O(n)。也就是说,当前数组区间中有 10 10 10 个元素时,我们大概操作 10 10 10 次就能找到基准值的位置了。清楚了单次操作的时间复杂度以后,我们就能知道总体的时间复杂度了。

首先,我们要确定总体时间复杂度的公式。我们用 T ( n ) T(n) T(n) 表示对 n n n 个元素的数组进行快速排序所用的时间,那么 T ( n ) T(n) T(n) 中应该包括了单次的 p a r t i t i o n partition partition 操作用时,以及 p a r t i t i o n partition partition 操作以后,我们对左右两个子数组分别做快速排序所用的时间,也就是 T ( n ) = n + T ( L ) + T ( R ) T(n) = n + T(L) + T(R) T(n)=n+T(L)+T(R)。其中 n n n 是单次 p a r t i t i o n partition partition 操作的用时, T ( L ) T(L) T(L) 和 T ( R ) T(R) T(R) 分别是对左右区间进行快速排序的用时, L L L 和 R R R 分别代表左区间和右区间中元素的数量。

接着,我们借助二叉树的结构来求一下 T ( n ) T(n) T(n)。首先,我们可以将基准值看成是由 n n n 个元素组成的二叉树的根节点,那么 p a r t i t i o n partition partition 操作就是找到这个根节点的正确位置,总用时就是 n n n。如果将这个用时 n n n 当做二叉树根节点的独立用时,那么左子树根节点的独立用时就是 L L L,右子树根节点的独立用时就是 R R R。这样,我们就得到了这个二叉树第二层上所有节点的独立用时: L + R = n − 1 L+R=n-1 L+R=n−1。我们可以将这个值大致看成是 n n n。依照此方法,你会得到接下来各层二叉树节点的独立用时,关系如图所示:


其中,每个节点上的数值代表了这个节点的独立用时。 n = L + R + 1 n = L+R+1 n=L+R+1, L = L L + L R + 1 L = LL+LR+1 L=LL+LR+1, R = R L + R R + 1 R = RL+RR+1 R=RL+RR+1。这也就意味着,第一层上节点的独立用时总和是 n n n,第二层上节点的独立用时总和是 L + R = n − 1 L+R=n-1 L+R=n−1,第三次上节点的独立用时总和是 L L + L R + R L + R R = n − 3 LL+LR+RL+RR=n-3 LL+LR+RL+RR=n−3。

其实,如果 n n n 足够大,那每一层上的所有节点的独立用时总和,我们都可以让其约等于 n n n。那快速排序的总用时,就可以约等于 n n n 乘上树的层数,也就是树的高度。因此,这棵树的高度越低,快速排序的效率越好,而树的高度越高,快速排序的效率也就越差。这样一来,我们就让树高这个直观的量和快速排序的效率有了概念上的关联。因此,我们只需要分析树高,就能分析清楚快速排序的执行效率了。

那针对有 n n n 个节点的二叉树(每个节点代表一个基准值, n n n 个节点就代表确定了 n n n 个基准值的位置),我们该怎么确定树高的范围呢?稍微一分析,你会发现,树高最低是 log ⁡ 2 ( n + 1 ) \log_{2}{(n+1)} log2​(n+1),也就是每个节点的左右两颗子树,所包含节点数量都差不多。这就意味着,我们每次选择基准值的时候,都要尽可能选择处在待排序数组中间的数字。也只有这样,快速排序算法才会达到最好的时间复杂度,也就是 n log ⁡ 2 n n\log_{2}{n} nlog2​n。

结合二叉树的结构,我们分析了快速排序的最好时间复杂度。而快速排序的最坏时间复杂度是 O ( n 2 ) O(n^2) O(n2),这又是怎么得到呢?从中你可以体会到:数据结构的价值,在于其思维逻辑结构层面的价值。

总的来说,上面关于时间复杂度的分析,虽然不是一个严谨的时间复杂度分析过程,可却是一个有效且正确的分析方法。所以,如果你之后有遇到相关算法,完全可以直接应用这样的方法去加深理解。

小结:

讲了快速排序的核心思想,也结合二叉树的结构一起分析了快排的时间复杂度。在这里,有两件需要记住的事情。

第一,理解 p a r t i t i o n partition partition 操作,是理解快速排序算法的关键。如果你 还没搞懂 p a r t i t i o n partition partition 操作的话,我建议你多花些时间把它搞清楚,这绝对是值得的。

第二,想要理解快排的时间复杂度,最有效的途径就是掌握二叉树分析法。其实这种分析方法,不仅可以用于分析我们今天所讲的快速排序算法,后面,当讲到归并排序算法的时候也能用得上。甚至可以说,掌握这种思维方式,是打通你算法与数据结构任督二脉的必经之路。

二、棋盘覆盖

在一个 2 k × 2 k 2k×2k 2k×2k 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的 4 4 4 种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何 2 2 2 个 L L L 型骨牌不得重叠覆盖。


当 k > 0 k>0 k>0 时,将 2 k × 2 k 2k×2k 2k×2k 棋盘分割为 4 4 4 个 2 k − 1 × 2 k − 1 2k-1×2k-1 2k−1×2k−1 子棋盘( a a a)所示。
特殊方格必位于 4 4 4 个较小子棋盘之一中,其余 3 3 3 个子棋盘中无特殊方格。为了将这 3 3 3 个无特殊方格的子棋盘转化为特殊棋盘,可以用一个 L L L 型骨牌覆盖这 3 3 3 个较小棋盘的会合处,如 ( b b b)所示,从而将原问题转化为 4 4 4 个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为棋盘 1 × 1 1×1 1×1。


源代码

源码一:

package SufaLjh.Exp1.Homework_3;import java.util.Arrays;public class qk {static int n = 10;/*** 快速排序的程序入口* @param args*/public static void main(String[] args) {int[] a = new int[n];//产生随机数,这里我使用random()方法随机生成0~99999内的10个整数for (int i = 0; i < n; i++) {a[i] = (int) ((Math.random())*100000);}System.out.println("排序前: ");//增加的for循环(foreach)for (int element:a) {System.out.print(element+" ");}int p = 0;int r = a.length-1;QuickSort(a,p,r);System.out.println();//换行System.out.println("排序后: ");System.out.println(Arrays.toString(a));//Output(a);}/*** 打印元素方法* @param a 数组*//*private static void Output(int[] a) {System.out.println("快速排序输出:");for (int i=0;i<a.length;i++){System.out.print(a[i]+" ");}System.out.println();}*//*** 时间复杂度为O(n*log n),空间复杂度为O(n*log n)* @param a 数组* @param startIndex 开始元素下标* @param endIndex 数组最后一个元素的下标*/private static void QuickSort(int[] a, int startIndex, int endIndex) {if (startIndex<endIndex){//找出基准值int q = Patition(a,startIndex,endIndex);//分成两边进行递归QuickSort(a,startIndex,q-1);QuickSort(a,q+1,endIndex);}}/*** 找基准元素* @param a 数组* @param startIndex 开始元素下标* @param endIndex 数组最后一个元素的下标* @return*/private static int Patition(int[] a, int startIndex, int endIndex) {int left = startIndex;int right = endIndex;int x = a[startIndex];//将<x的元素交换到左边区域//将>x的元素交换到右边区域while(true){//控制right指针比较并左移while(left<right&&a[right]>x){right--;}//控制left指针比较并右移while(left<right&&a[left]<=x){left++;}//找到left比基准值大,right比基准值小的元素,进行交换if (left>=right){break;}else{//交换left和right指针所指向的元素swap(a,left,right);}}//完成第一轮,让left 和 right重合的位置和基准交换,返回基准的位置(left)a[startIndex] = a[left];a[left] = x;return left;}/*** 交换两个数* @param a* @param left* @param right*/private static void swap(int[] a, int left, int right) {int p = a[left];a[left] = a[right];a[right] = p;}
}

运行结果一:

源码二:

package SufaLjh.Exp1.Homework_3;import javax.swing.*;
import java.awt.*;public class qipan extends JFrame {static int n =8;int[][] qi_pan= new int[n][n];//8*8的棋盘static int tile = 1; //L型骨牌编号/*** 棋盘覆盖函数入口* @param args*/public static void main(String[] args) {new qipan().setVisible(true);}/***  设置棋盘的位置和规格*/public qipan(){this.setSize(400,400);this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);this.setVisible(true);chessBoard(0,0,3,3,n);for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {System.out.printf("%5d ",qi_pan[i][j]);}System.out.println();}this.setLayout(null);this.repaint();}/*** 找特殊点坐标* @param tr 特殊点的行下标* @param tc 特殊点的列下标* @param dr 矩阵的左边起点行下标* @param dc 矩阵左边起点的列下标* @param size 矩阵的宽或高*/private void chessBoard(int tr, int tc, int dr, int dc, int size) {if (size == 1) return;int t = tile++;//L 型骨牌号int s=size/2;//分割棋盘//覆盖左上角子棋盘if (dr<tr+s&&dc<tc+s){//特殊方格在此棋盘中chessBoard(tr,tc,dr,dc,s);}else{//此棋盘中无特殊方格//用t号L型骨牌覆盖右下角qi_pan[tr+s-1][tc+s-1] = t;//覆盖其余方格chessBoard(tr,tc,tr+s-1,tc+s-1,s);}//覆盖右上角子棋盘if (dr<tr+s&&dc>=tc+s){//特殊方格在此棋盘中chessBoard(tr,tc+s,dr,dc,s);}else{//此棋盘中无特殊方格// 用t号L型骨牌覆盖左下角qi_pan[tr+s-1][tc+s] = t;//覆盖其余方格chessBoard(tr,tc+s,tr+s-1,tc+s,s);}//覆盖左下角子棋盘if (dr>=tr+s&&dc<tc+s){//特殊方格在此棋盘中chessBoard(tr+s,tc,dr,dc,s);}else{//此棋盘中无特殊方格//用t号L型骨牌覆盖右上角qi_pan[tr+s][tc+s-1] = t;//覆盖其余方格chessBoard(tr+s,tc,tr+s,tc+s-1,s);}//覆盖右下角子棋盘if (dr>=tr+s&&dc>=tc+s){//特殊方格在此棋盘中chessBoard(tr+s,tc+s,dr,dc,s);}else{//此棋盘中无特殊方格//用t号L型骨牌覆盖左上角qi_pan[tr+s][tc+s] = t;//覆盖其余方格chessBoard(tr+s,tc+s,tr+s,tc+s,s);}}/*** 重写画图方法* @param g*/@Overridepublic void paint(Graphics g) {super.paint(g);for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {g.setColor(new Color((qi_pan[i][j]*47+255)%256,(qi_pan[i][j]*61+255)%256,255));g.fill3DRect(i*30+130,j*30+130,29,29,true);}}}
}

运行结果二:


源码下载

快速排序

棋盘覆盖

分治法:快速排序棋盘覆盖相关推荐

  1. 分治法解决棋盘覆盖问题

    分治法解决棋盘覆盖问题 问题描述: 在一个2k×2k(k≥0)个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为特殊方格.显然,特殊方格在棋盘中出现的位置有4k中情形,因而有4k中不同的棋盘. ...

  2. 【分治法】棋盘覆盖问题java实现

    文章目录 问题描述 问题分析 算法设计 java代码 问题描述 在一个2k × 2k个放个中,恰好只有一个方格是残缺的.也就是在这个棋盘中有一个方格与其它的格子不同, 我们称这种棋盘为残缺棋盘. 下图 ...

  3. 「分治法」棋盘覆盖问题

    一.问题描述 在一个2k×2 k(k≥0)个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为特殊方格. 棋盘覆盖问题要求用图所示的4种不同形状的L型骨牌覆盖给定棋盘上除特殊方格以外的所有方格, ...

  4. 分治法 —— 快速排序和归并排序(自底向上和自顶向下)

    问题: 对于给定的含有n个元素的数组a,对其按元素值递增排序. - 快速排序 1.基本思想: 划分:在待排序的n个元素中任取一个元素(通常取第一个元素)作为基准,把该元素放入最终元素后,整个数据 序列 ...

  5. 分治法 —— 快速排序(递归,迭代,非递归)

    快速排序 快速排序三种方法,你值得参考!!! 快速排序 1,快速排序之递归 Code 递归 2,快速排序之迭代 Code 迭代 3,快速排序之非递归 Code 非递归 4,结语 快速排序在所有排序中基 ...

  6. java棋盘覆盖分治法,棋盘覆盖-分治法

    信 息 工 程 学 院 算法分析 实习报告 学院:信息工程学院 班级:软件工程083 姓名: 学号: 成绩: 一.实习题目 : 棋盘覆盖 二.实习过程 : 1.了解分治法的思想: 将一个难以解决的大问 ...

  7. 计算机基础算法棋盘覆盖,分治算法求解棋盘覆盖问题互动教学过程.doc

    分治算法求解棋盘覆盖问题互动教学过程 分治算法求解棋盘覆盖问题互动教学过程 摘要:针对算法设计与分析课程难度较大.对学生编程能力要求较高的现状,通过对棋盘覆盖问题的分治算法求解过程进行互动教学设计,引 ...

  8. 棋盘覆盖问题--分治策略

    问题描述: 在一个2k×2k (k≥0,k为上标)个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为特殊方格.棋盘覆盖问题要求用图(b)所示的4种不同形状的L型骨牌覆盖给定棋盘上除特殊方格以外 ...

  9. python棋盘覆盖问题,python实现棋盘覆盖问题及可视化

    问题介绍 棋盘覆盖问题,是一种编程问题. 如何应用分治法求解棋盘覆盖问题呢?分治的技巧在于如何划分棋盘,使划分后的子棋盘的大小相同,并且每个子棋盘均包含一个特殊方格,从而将原问题分解为规模较小的棋盘覆 ...

最新文章

  1. Firefox3 RC1颁布各种新特征发扬阐发更平定
  2. Nature Method :Rob Knight发布Striped UniFrac算法轻松分析微生物组大数据
  3. linux shell编程学习笔记(9)正则表达式
  4. 波司登在“寒潮”下再创新高,羽绒服行业真的靠天吃饭?
  5. 科大星云诗社动态20210222
  6. 用隐马尔可夫模型(HMM)做命名实体识别——NER系列(二)
  7. 机器学习笔记2 – sklearn之iris数据集
  8. MTK 驱动(85)----RPMB key introduction
  9. 《InsideUE4》GamePlay 架构(二)Level 和 World
  10. python读取us7ascii字符集Oracle数据库中文乱码问题的解决方案
  11. C#获取系统当前时间
  12. 松下plc编程线usb驱动
  13. html设置了背景图片不显示,CSS设置背景图片不显示的解决方法
  14. 在我笔记本Ubuntu上装普罗米修斯记录
  15. 大英博物馆天猫开店,本王的宝贝都要被你们玩坏啦!
  16. 2022-2027年中国建筑施工机械租赁市场规模预测及投资战略咨询报告
  17. 计蒜客 428(人人都有极客精神-日期问题)
  18. 让Word 2007默认文档保存格式为Word 2003的DOC格式
  19. 用户标签体系的搭建方法
  20. linux安装使用jq

热门文章

  1. 学习篇——了解OKR
  2. 移动设备管理软件优劣,南京烽火星空来判别
  3. 不堪回首的真实往事:我和一个骗子网友的两年矛盾纠葛
  4. 一加7t人脸识别_600美元起售:一加7T真机抢先看 90Hz屏/环形3摄
  5. 标准日本语初级 语法整理
  6. 抽象类:小样儿(接口),我一眼看出你就不是人(抽象类)
  7. 工控机主板与ARM工控机主板有什么不同呢?
  8. http://nian.so/#网站的拓展工具编写
  9. AI MAX交互式开发使用方法说明(配合xshell)
  10. 拉格朗日乘数法求解技巧2