十大基础算法

来源:互联网 发布:电影推荐2017知乎 编辑:程序博客网 时间:2024/06/05 03:59

快速排序

在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

<1>.从数列中挑出一个元素,称为 "基准"(pivot);

<2>.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。

在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

<3>.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

算法思想:以军训排队为例,教官说以第一个同学为中心,比他矮的站他左边,比他高的站他右边,这就是一趟快速排序。因此,一趟快速排序是以一个枢轴,将序列分成两部分,枢轴的一边比它小(或小于等于),另一边比它大(或大于等于)。
代码实现:

public static int[] quickSort(int[] A, int n) {    // write code here    qsort(A, 0, n - 1);    return A;}private static void qsort(int[] arr, int lo, int hi) {    if (hi <= lo) {        return;    }    int lt = lo;         //存储小于切分元素的指针    int i = lo + 1;      //存储等于切分元素的指针    int gt = hi;         //存储大于切分元素的指针    int split = arr[lo]; //切分元素    while (i <= gt) {        if (arr[i] < split) {            int temp = arr[lt];            arr[lt] = arr[i];            arr[i] = temp;            i++;            lt++;        } else if (arr[i] > split) {            int temp = arr[gt];            arr[gt] = arr[i];            arr[i] = temp;            gt--;        } else {            i++;        }    }//至此,a[lo...lt-1] < v=a[lt...gt] < a[gt+1...hi]    qsort(arr, lo, lt - 1);    qsort(arr, gt + 1, hi);}

堆排序

利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的平均时间复杂度为Ο(nlogn) 。
具体算法描述如下:

<1>.将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区;

<2>.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n];

<3>.由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

算法思想:堆是一种数据结构,最好的理解堆的方式就是把堆看成一棵完全二叉树,这个完全二叉树满足任何一个非叶节点的值,都不大于(或不小于)其左右孩子节点的值。若父亲大孩子小,则这样的堆叫做大顶堆;若父亲小孩子大,这样的堆叫做小顶堆。根据堆的定义,其根节点的值是最大(或最小),因此将一个无序序列调整为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个值交换到序列的最后(或最前),这样有序序列元素增加1个,无序序列中元素减少1个,对新的无序序列重复这样的操作,就实现了序列排序。堆排序中最关键的操作是将序列调整为堆,整个排序的过程就是通过不断调整使得不符合堆定义的完全二叉树变为符合堆定义的完全二叉树的过程。
堆排序执行过程(大顶堆):

(1)从无序序列所确定的完全二叉树的第一个非叶子节点开始,从右至左,从下至上,对每个节点进行调整,最终将得到一个大顶堆。将当前节点(a)的值与其孩子节点进行比较,如果存在大于a值的孩子节点,则从中选出最大的一个与a交换。当a来到下一层的时候重复上述过程,直到a的孩子节点值都小于a的值为止。

(2)将当前无序序列中第一个元素,在树中是根节点(a)与无序序列中最后一个元素(b)交换。a进入有序序列,到达最终位置,无序序列中元素减少1个,有序序列中元素增加1个,此时只有节点b可能不满足堆的定义,对其进行调整。

(3)重复过程2,直到无序序列中的元素剩下1个时排序结束。

代码实现:

public static int[] heapSort(int[] A, int n) {    // write code here    for (int k = n/2; k >= 1; k--){        sink(A, k, n);    }    while (n > 1) {        exch(A, 1, n--);        sink(A, 1, n);    }    return A;}private static void sink(int[] A, int k, int n) {    while (2*k <= n) {        int j = 2*k;        if (j < n && A[j-1] < A[j]) {            j++;        }        if (A[k-1] >= A[j-1]) {            break;        }        exch(A, k, j);        k = j;    }}private static void exch(int[] A, int i, int j) {    int swap = A[i-1];    A[i-1] = A[j-1];    A[j-1] = swap;}

归并排序

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
算法简介
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
具体算法描述如下:

把长度为n的输入序列分成两个长度为n/2的子序列;

对这两个子序列分别采用归并排序;

将两个排序好的子序列合并成一个最终的排序序列。


算法思想:其核心就是“两两归并”,首先将原始序列看成每个只含有单独1个元素的子序列,两两归并,形成若干有序二元组,则第一趟归并排序结束,再将这个序列看成若干个二元组子序列,继续两两归并,形成若干有序四元组,则第二趟归并排序结束,以此类推,最后只有两个子序列,再进行一次归并,即完成整个归并排序。
代码实现:

public static int[] mergeSort(int[] A, int n) {    aux = new int[n];    sort(A, 0, n - 1);    return A;}private static void sort(int[] arr, int left, int right) {    if (right <= left) {        return;    }    int middle = left + ((right - left) >> 1);    sort(arr, left, middle);//左侧排序    sort(arr, middle + 1, right);//右侧排序    merge(arr, left, middle, right);}private static void merge(int[] arr, int left, int middle, int right) {    int lo = left;    int mid = middle + 1;    for (int k = lo; k <= right; k++) {//将数组arr的lo到right位复制到数组aux中        aux[k] = arr[k];    }    for (int k = lo; k <= right; k++) {//归并到数组arr中        if (lo > middle) {             //左半边元素用尽,取右半边            arr[k] = aux[mid++];        } else if (mid > right) {   //右半边元素用尽,取左半边            arr[k] = aux[lo++];        } else if (aux[mid] < aux[lo]) {//右半边元素小于左半边元素,取右半边            arr[k] = aux[mid++];        } else {                      //右半边元素大于等于左半边元素,取左半边            arr[k] = aux[lo++];        }    }}

二分查找算法

二分查找算法是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜 素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组 为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。折半搜索每次把搜索区域减少一半,时间复杂度为Ο(logn) 。

public static int rank(int key, int[] a) {    //前提:数组必须是有序的    int low = 0;    int high = a.length - 1;    //等号不能漏掉    while (low <= high) {        int mid = low + (high - low) / 2;        if (key < a[mid]) high = mid - 1;        else if (key > a[mid]) low = mid + 1;        else return mid;    }    return -1;}

DFS(深度优先搜索)

是搜索算法的一种。它沿着树的深度遍历树的节点,尽可能深的搜索树的分 支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发 现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。DFS属于盲目搜索。深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。一般用堆数据结构来辅助实现DFS算法。
算法步骤:
  • 访问顶点v;
  • 依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
  • 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
伪代码实现:
void search(Node root){if(root == null) return;visit(root);root.visited = true;//标记该结点已被遍历for(Node n : root.adjacent){//依次遍历该结点的所有相邻节点     if(n.visited == false)     search(n);//继续遍历相邻结点的相邻结点}}

DFS(深度优先搜索)

是一种图形搜索算法。简单的说,BFS是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。如果所有节点均被访问,则算法中止。BFS同样属于盲目搜索。一般用队列数据结构来辅助实现BFS算法。
算法步骤:
  1. 首先将根节点放入队列中。
  2. 从队列中取出第一个节点,并检验它是否为目标。
    1. 如果找到目标,则结束搜寻并回传结果。
    2. 否则将它所有尚未检验过的直接子节点加入队列中。
  3. 若队列为空,表示整张图都检查过了——亦即图中没有欲搜寻的目标。结束搜寻并回传“找不到目标”。
  4. 重复步骤2。
位代码实现:
void search(Node root){Queue queue = new Queue();root.visited = true;visit(root);queue.enqueue(root);//将节点添加到队尾while(!queue.isEmpty){Node r = queue.dequeue();//从队列头移出for(Node n : r.adjacent){visit(n);n.visited = true;queue.enqueue(n);}}}

动态规划算法

动态规划算法通常基于一个递推公式及一个或多个初始状态。 当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度, 因此它比回溯法、暴力法等要快许多。
现在让我们通过一个例子来了解一下DP的基本原理。
首先,我们要找到某个状态的最优解,然后在它的帮助下,找到下一个状态的最优解。

“状态”代表什么及如何找到它?

“状态”用来描述该问题的子问题的解。

如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元? (表面上这道题可以用贪心算法,但贪心算法无法保证可以求出解,比如1元换成2元的时候)
首先我们思考一个问题,如何用最少的硬币凑够i元(i<11)?为什么要这么问呢? 两个原因:

  1. 当我们遇到一个大问题时,总是习惯把问题的规模变小,这样便于分析讨论。
  2. 这个规模变小后的问题和原来的问题是同质的,除了规模变小,其它的都是一样的, 本质上它还是同一个问题(规模变小后的问题其实是原问题的子问题)。
好了,让我们从最小的i开始吧。当i=0,即我们需要多少个硬币来凑够0元。 由于1,3,5都大于0,即没有比0小的币值,因此凑够0元我们最少需要0个硬币。 这时候我们发现用一个标记来表示这句“凑够0元我们最少需要0个硬币。”会比较方便, 如果一直用纯文字来表述,不出一会儿你就会觉得很绕了。
那么, 我们用d(i)=j来表示凑够i元最少需要j个硬币。于是我们已经得到了d(0)=0, 表示凑够0元最小需要0个硬币。
  • 当i=1时,只有面值为1元的硬币可用, 因此我们拿起一个面值为1的硬币,接下来只需要凑够0元即可,而这个是已经知道答案的, 即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。
  • 当i=2时, 仍然只有面值为1的硬币可用,于是我拿起一个面值为1的硬币, 接下来我只需要再凑够2-1=1元即可(记得要用最小的硬币数量),而这个答案也已经知道了。 所以d(2)=d(2-1)+1=d(1)+1=1+1=2。
  • 当i=3时,我们能用的硬币就有两种了,1元的和3元的。 既然能用的硬币有两种,我就有两种方案:
    • 如果我拿了一个1元的硬币,我的目标就变为了: 凑够3-1=2元需要的最少硬币数量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。 这个方案说的是,我拿3个1元的硬币;
    • 我拿起一个3元的硬币, 我的目标就变成:凑够3-3=0元需要的最少硬币数量。即d(3)=d(3-3)+1=d(0)+1=0+1=1. 这个方案说的是,我拿1个3元的硬币。
好了,这两种方案哪种更优呢? 记得我们可是要用最少的硬币数量来凑够3元的。所以, 选择d(3)=1,怎么来的呢?具体是这样得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。
从以上的文字中, 我们要抽出动态规划里非常重要的两个概念:状态和状态转移方程。
上文中d(i)表示凑够i元需要的最少硬币数量,我们将它定义为该问题的”状态”, 这个状态是怎么找出来的呢? 根据子问题定义状态。你找到子问题,状态也就浮出水面了。 最终我们要求解的问题,可以用这个状态来表示:d(11),即凑够11元最少需要多少个硬币。 那状态转移方程是什么呢?既然我们用d(i)表示状态,那么状态转移方程自然包含d(i), 上文中包含状态d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。没错, 它就是状态转移方程,描述状态之间是如何转移的。当然,我们要对它抽象一下,
d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j个硬币的面值;
有了状态和状态转移方程,这个问题基本上也就解决了。
代码:
/*** 有数组penny,penny中所有的值都为正数且不重复。* 每个值代表一种面值的货币,每种面值的货币可以使用任意张,* 再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。* 给定数组penny及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。*/private static int countWays(int[] penny, int n, int aim) {    // write code here    int[][] map = new int[n][aim + 1];    //初始化第一列。组成面值为0的情况,无论使用任何种货币,只能有一种组合方式,那就是不使用任何面值的货币    for (int i = 0; i < n; i++) {        map[i][0] = 1;    }    //初始化第一行。当前面值可以整除第一个货币的面值,则有一种一种组合方式    for (int i = 1; i < aim + 1; i++) {        if (i % penny[0] == 0) {            map[0][i] = 1;        }    }    //填充map数组    for (int i = 1; i < n; i++) {//遍历所有行        for (int j = 1; j < aim + 1; j++) {//遍历所有列            if (j >= penny[i]) {//当前面值不小于当前使用的货币                //当前面值=不使用当前面值的情况+使用当前面值的情况                map[i][j] = map[i - 1][j] + map[i][j - penny[i]];            } else {//当前面值小于当前使用的货币                //当前面值=不使用当前面值的情况                map[i][j] = map[i - 1][j];            }        }    }    return map[n-1][aim];}

Dijkstra算法

迪科斯彻算法使用了广度优先搜索解决非负权有向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。
该算法的输入包含了一个有权重的有向图 G,以及G中的一个来源顶点 S。我们以 V 表示 G 中所有顶点的集合。每一个图中的边,都是两个顶点所形成的有序元素对。(u, v) 表示从顶点 u 到 v 有路径相连。我们以 E 表示G中所有边的集合,而边的权重则由权重函数 w: E → [0, ∞] 定义。因此,w(u, v) 就是从顶点 u 到顶点 v 的非负权重(weight)。边的权重可以想像成两个顶点之间的距离。任两点间路径的权重,就是该路径上所有边的权重总和。已知有 V 中有顶点 s 及 t,Dijkstra 算法可以找到 s 到 t的最低权重路径(例如,最短路径)。这个算法也可以在一个图中,找到从一个顶点 s 到任何其他顶点的最短路径。对于不含负权的有向图,Dijkstra算法是目前已知的最快的单源最短路径算法。
算法步骤:
  1. 初始时令 S={V0},T={其余顶点},T中顶点对应的距离值
    1. 若存在<V0,Vi>,d(V0,Vi)为<V0,Vi>弧上的权值
    2. 若不存在<V0,Vi>,d(V0,Vi)为∞
  2. 从T中选取一个其距离值为最小的顶点W且不在S中,加入S
  3. 对其余T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值缩短,则修改此距离值
  4. 重复上述步骤2、3,直到S中包含所有顶点,即W=Vi为止
图解:

以上图为例,以点D作为起点进行说明


将顶点D加入到S中。
此时,S={D(0)}, U={A(∞),B(∞),C(3),E(4),F(∞),G(∞)}。


此时C点距离D最近,所以将C点添加到S集合中,并更新U集合:S={D(0),C(3)}, U={A(∞),B(13),E(4),F(9),G(∞)}。


此时E点距离D最近,所以将E点添加到S集合中,并更新U集合:S={D(0),C(3),E(4)}, U={A(∞),B(13),F(6),G(12)}。


此时F点距离D最近,所以将F点添加到S集合中,并更新U集合:S={D(0),C(3),E(4),F(6)}, U={A(22),B(13),G(12)}。


此时G点距离D最近,所以将G点天极道S集合中,并更新U集合:S={D(0),C(3),E(4),F(6),G(12)}, U={A(22),B(13)}。


此时B点距离D最近,所以将B点天极道S集合中,并更新U集合:S={D(0),C(3),E(4),F(6),G(12),B(13)}, U={A(22)}。


此时A点距离D最近,所以将A点天极道S集合中,并更新U集合:S={D(0),C(3),E(4),F(6),G(12),B(13),A(22)}。

此时,集合U为空,图中所有节点便利完毕。
代码实现:

/* * Dijkstra最短路径。 * 即,统计图中"顶点vs"到其它各个顶点的最短路径。 * * 参数说明: *       vs -- 起始顶点(start vertex)。即计算"顶点vs"到其它顶点的最短路径。 *     prev -- 前驱顶点数组。即,prev[i]的值是"顶点vs"到"顶点i"的最短路径所经历的全部顶点中,位于"顶点i"之前的那个顶点。 *     dist -- 长度数组。即,dist[i]是"顶点vs"到"顶点i"的最短路径的长度。 */public void dijkstra(int vs, int[] prev, int[] dist) {    // flag[i]=true表示"顶点vs"到"顶点i"的最短路径已成功获取    boolean[] flag = new boolean[mVexs.length];    // 初始化    for (int i = 0; i < mVexs.length; i++) {        flag[i] = false;          // 顶点i的最短路径还没获取到。        prev[i] = 0;              // 顶点i的前驱顶点为0。        dist[i] = mMatrix[vs][i];  // 顶点i的最短路径为"顶点vs"到"顶点i"的权。    }    // 对"顶点vs"自身进行初始化    flag[vs] = true;    dist[vs] = 0;    // 遍历mVexs.length-1次;每次找出一个顶点的最短路径。    int k=0;    for (int i = 1; i < mVexs.length; i++) {        // 寻找当前最小的路径;        // 即,在未获取最短路径的顶点中,找到离vs最近的顶点(k)。        int min = INF;        for (int j = 0; j < mVexs.length; j++) {            if (flag[j]==false && dist[j]<min) {                min = dist[j];                k = j;            }        }        // 标记"顶点k"为已经获取到最短路径        flag[k] = true;        // 修正当前最短路径和前驱顶点        // 即,当已经"顶点k的最短路径"之后,更新"未获取最短路径的顶点的最短路径和前驱顶点"。        for (int j = 0; j < mVexs.length; j++) {            int tmp = (mMatrix[k][j]==INF ? INF : (min + mMatrix[k][j]));            if (flag[j]==false && (tmp<dist[j]) ) {                dist[j] = tmp;                prev[j] = k;            }        }    }    // 打印dijkstra最短路径的结果    System.out.printf("dijkstra(%c): \n", mVexs[vs]);    for (int i=0; i < mVexs.length; i++)        System.out.printf("  shortest(%c, %c)=%d\n", mVexs[vs], mVexs[i], dist[i]);}

朴素贝叶斯分类算法

朴素贝叶斯分类算法是一种基于贝叶斯定理的简单概率分类算法。贝叶斯分类的基础是概率推理,就是在各种条件的存在不确定,仅知其出现概率的情况下, 如何完成推理和决策任务。概率推理是与确定性推理相对应的。而朴素贝叶斯分类器是基于独立假设的,即假设样本每个特征与其他特征都不相关。朴素贝叶斯分类器依靠精确的自然概率模型,在有监督学习的样本集中能获取得非常好的分类效果。在许多实际应用中,朴素贝叶斯模型参数估计使用最大似然估计方法,换言之朴素贝叶斯模型能工作并没有用到贝叶斯概率或者任何贝叶斯模型。

尽管是带着这些朴素思想和过于简单化的假设,但朴素贝叶斯分类器在很多复杂的现实情形中仍能够取得相当好的效果。
朴素贝叶斯分类的步骤如下:
  1. 设为一个待分类项,而每个a为x的一个特征属性。
  2. 有类别集合。
  3. 计算。
  4. 如果,则
那么现在的关键就是如何计算第3步中的各个条件概率。我们可以这么做:
  1. 找到一个已知分类的待分类项集合,这个集合叫做训练样本集。
  2. 统计得到在各类别下各个特征属性的条件概率估计。即。
  3. 如果各个特征属性是条件独立的,则根据贝叶斯定理有如下推导:

因为分母对于所有类别为常数,因为我们只要将分子最大化皆可。又因为各特征属性是条件独立的,所以有:

FPRT(线性查找算法)

BFPRT算法解决的问题十分经典,即从某n个元素的序列中选出第k大(第k小)的元素,通过巧妙的分 析,BFPRT可以保证在最坏情况下仍为线性时间复杂度。该算法的思想与快速排序思想相似,当然,为使得算法在最坏情况下,依然能达到o(n)的时间复杂 度,五位算法作者做了精妙的处理。
算法步骤:
  1. 将n个元素每5个一组,分成n/5(上界)组。
  2. 取出每一组的中位数,任意排序方法,比如插入排序。
  3. 递归的调用selection算法查找上一步中所有中位数的中位数,设为x,偶数个中位数的情况下设定为选取中间小的一个。
  4. 用x来分割数组,设小于等于x的个数为k,大于x的个数即为n-k。
  5. 若i==k,返回x;若i<k,在小于x的元素中递归查找第i小的元素;若i>k,在大于x的元素中递归查找第i-k小的元素。
  6. 终止条件:n=1时,返回的即是i小元素。
代码实现:
以寻找最小的K个数为例(代码来自左程云老师算法视频)

public static int[] getMinKNumsByBFPRT(int[] arr, int k) {if (k < 1 || k > arr.length) {return arr;}int minKth = getMinKthByBFPRT(arr, k);int[] res = new int[k];int index = 0;for (int i = 0; i != arr.length; i++) {if (arr[i] < minKth) {res[index++] = arr[i];}}for (; index != res.length; index++) {res[index] = minKth;}return res;}public static int getMinKthByBFPRT(int[] arr, int K) {int[] copyArr = copyArray(arr);return select(copyArr, 0, copyArr.length - 1, K - 1);}public static int[] copyArray(int[] arr) {int[] res = new int[arr.length];for (int i = 0; i != res.length; i++) {res[i] = arr[i];}return res;}public static int select(int[] arr, int begin, int end, int i) {if (begin == end) {return arr[begin];}int pivot = medianOfMedians(arr, begin, end);int[] pivotRange = partition(arr, begin, end, pivot);if (i >= pivotRange[0] && i <= pivotRange[1]) {return arr[i];} else if (i < pivotRange[0]) {return select(arr, begin, pivotRange[0] - 1, i);} else {return select(arr, pivotRange[1] + 1, end, i);}}public static int medianOfMedians(int[] arr, int begin, int end) {int num = end - begin + 1;int offset = num % 5 == 0 ? 0 : 1;int[] mArr = new int[num / 5 + offset];for (int i = 0; i < mArr.length; i++) {int beginI = begin + i * 5;int endI = beginI + 4;mArr[i] = getMedian(arr, beginI, Math.min(end, endI));}return select(mArr, 0, mArr.length - 1, mArr.length / 2);}public static int[] partition(int[] arr, int begin, int end, int pivotValue) {int small = begin - 1;int cur = begin;int big = end + 1;while (cur != big) {if (arr[cur] < pivotValue) {swap(arr, ++small, cur++);} else if (arr[cur] > pivotValue) {swap(arr, cur, --big);} else {cur++;}}int[] range = new int[2];range[0] = small + 1;range[1] = big - 1;return range;}public static int getMedian(int[] arr, int begin, int end) {insertionSort(arr, begin, end);int sum = end + begin;int mid = (sum / 2) + (sum % 2);return arr[mid];}public static void insertionSort(int[] arr, int begin, int end) {for (int i = begin + 1; i != end + 1; i++) {for (int j = i; j != begin; j--) {if (arr[j - 1] > arr[j]) {swap(arr, j - 1, j);} else {break;}}}}public static void swap(int[] arr, int index1, int index2) {int tmp = arr[index1];arr[index1] = arr[index2];arr[index2] = tmp;}public static void printArray(int[] arr) {for (int i = 0; i != arr.length; i++) {System.out.print(arr[i] + " ");}System.out.println();}