十大排序算法
参考菜鸟教程:https://www.runoob.com/w3cnote/ten-sorting-algorithm.html
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用以下两张图概括:
名词解释:
- n:数据规模
- k:"桶"的个数
- In-place:占用常数内存,不占用额外内存
- Out-place:占用额外内存
- 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
1.冒泡排序(Bubble Sort)
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。其基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n) | O( n) | O(n2) | O(1) | 稳定 |
1.1 算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1.2 复杂度分析
- 最好的情况:当输入的数据已经是正序,即最大的数在最后。此时只进行n-1次的比较,没有数据进行交换,时间复杂度为O(n)。
- 最坏的情况:当输入的数据是反序的时候,需要比较的次数为: ∑ i = 2 n ( i − 1 ) = 1 + 2 + 3 + ⋅ ⋅ ⋅ + ( n − 1 ) = n ( n − 1 ) 2 \sum_{i=2}^n(i-1) = 1+2+3+···+(n -1)=\frac{n(n-1)}{2} i=2∑n(i−1)=1+2+3+⋅⋅⋅+(n−1)=2n(n−1)
- 时间复杂度:
最佳情况:T(n) = O(n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2) - 空间复杂度:
O(1)
1.3 Java代码实现以及参考练习
class BubbleSort{
public static int[] bubbleSort(int[] nums){
int temp=0; // 用于交换临时存储值
for(int i=0;i<nums.length-1;i++){
int swap = 0; //设置为发生交换标志,进行剪枝
for(int j=0;j<nums.length-1-i;j++){
if(nums[j]>nums[j+1]){
temp=nums[j];
nums[j]=nums[j+1];
nums[j+1]=temp;
swap=1; //有交换发生
}
}
if(swap==0) break; //本趟比较中未出现交换则结束排序
}
return nums;
}
}
2.选择排序(Selection Sort)
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。它最大的特点就是交换移动数据的次数相当少。最好的情况下,移动0次;最差的情况下,交换次数也仅为n-1次。尽管与冒泡排序时间复杂度同为O(n2),但是性能还是略优于冒泡排序。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n) | O(n2) | O(n2) | O(1) | 不稳定 |
2.1 算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
2.2 复杂度分析
无论最好最差的情况,选择排序的比较次数都是一样多的,需要比较的次数为: ∑ i = 2 n ( i − 1 ) = 1 + 2 + 3 + ⋅ ⋅ ⋅ + ( n − 1 ) = n ( n − 1 ) 2 \sum_{i=2}^n(i-1) = 1+2+3+···+(n -1)=\frac{n(n-1)}{2} i=2∑n(i−1)=1+2+3+⋅⋅⋅+(n−1)=2n(n−1)
- 时间复杂度为O(n2)
- 空间复杂度为O(1)
2.3 Java代码实现以及参考练习
LC 215. 数组中的第K个最大元素
LC 912. 排序数组
class SelectionSort{
public static int[] SelectionSort(int[] nums){
if (nums.length == 0) return nums;
for(int i = 0; i < nums.length - 1; i++){ // 总共要经过 N-1 轮比较
int minIndex = i;
for(int j = i + 1; j < nums.length; j++){ // 每轮需要比较的次数 N-i
if(nums[j] < nums[minIndex]){
minIndex = j; // 记录目前能找到的最小值元素的下标
}
}
int temp = nums[minIndex];
nums[minIndex] = nums[i];
nums[i] = temp;
}
return nums;
}
}
3.插入排序(Insert Sort)
插入排序是一种最简单直观的排序算法,它的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
插入排序适合于小序列的排序,当序列长度小于等于20或20左右时,使用插入排序效率更高。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n2) | O(n) | O(n2) | O(1) | 稳定 |
3.1 算法步骤
- 将一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
3.2 复杂度分析
- 最好的情况:当输入的数据已经是正序,即最大的数在最后。此时只进行n-1次的比较,没有数据的移动,时间复杂度为O(n)。
- 最坏的情况:当输入的数据是反序的时候,需要比较的次数为:
∑ i = 2 n i = 2 + 3 + ⋅ ⋅ ⋅ + n = ( n + 2 ) ( n − 1 ) 2 \sum_{i=2}^ni = 2+3+···+ n=\frac{(n+2)(n-1)}{2} i=2∑ni=2+3+⋅⋅⋅+n=2(n+2)(n−1)
移动的次数为:
∑ i = 2 n ( i + 1 ) = ( n + 4 ) ( n − 1 ) 2 \sum_{i=2}^n(i+1) =\frac{(n+4)(n-1)}{2} i=2∑n(i+1)=2(n+4)(n−1)
- 时间复杂度为O(n2)
- 空间复杂度为O(1)
插入排序的性能比冒泡和选择排序要高一些
3.3 Java代码实现以及参考练习
LC 912. 排序数组
LC 147. 对链表进行插入排序
class Insertsort{
public static int[] Insertsort(int[] arr){
// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
for (int i = 1; i < arr.length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从已经排序的序列最右边的开始比较,找到比其小的数
int j = i;
while (j > 0 && tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
// 存在比其小的数,插入
if (j != i) {
arr[j] = tmp;
}
}
return arr;
}
}
4.希尔排序(Shell Sort)
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序的算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n log n) | O(n log2 n) | O(n log2 n) | O(1) | 不稳定 |
4.1 算法步骤
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 复杂度分析
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。需要注意的是,增量序列的最后一个增量必须等于1。
- 时间复杂度为O(n logn)
- 空间复杂度为O(1)
4.3 Java代码实现以及参考练习
class SortArray{
public static int[] sortArray(int[] nums) {
int length = nums.length;
int temp;
for(int step = length / 2; step >= 1; step /= 2){
for(int i = step; i < length; i++){
temp = nums[i];
int j = i;
while(j >= step && nums[j - step] > temp){
nums[j] = nums[j - step];
j -= step;
}
nums[j] = temp;
}
}
return nums;
}
}
}
5.归并排序(Merge Sort)
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
它的原理是:假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]([x]表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,···,重复到有一个长度为n的有序子序列为止。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
5.1 算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
5.2 复杂度分析
- 时间复杂度为O(n logn)
- 空间复杂度为O(n)
5.3 Java代码实现以及参考练习
LC 剑指Offer 51. 数组中的逆序对
LC 面试题10.01. 合并排序的数组
class MergeSort{
public static int[] mergeSortMain(int[] arr) {
// 空数组 或 只有一个元素的数组,则什么都不做。
if (arr == null || arr.length <= 1) return arr;
mergeSort(arr, 0, arr.length - 1);
return arr;
}
private static void mergeSort(int[] arr, int left, int right){
if (left>= right) return;
// 计算出中间值,这种算法保证不会溢出。
int mid = left+ ((right - left) >> 1);
// 先对左边排序
mergeSort(arr, left, mid);
// 先对右边排序
mergeSort(arr, mid + 1,right );
// 归并两个有序的子序列
merge(arr, left, mid, right );
}
private static void merge(int[] arr, int low, int mid, int high) {
// temp[]是临时数组,包左不包右,所以要额外 + 1。
int[] temp = new int[high - low + 1];
int left = low; // 左侧指针从low开始。
int right = mid + 1; // 右侧指针从mid+1开始。
int index = 0; // 此索引用于temp[]
// 当两个子序列还有元素时,从小到大放入temp[]中。
while (left <= mid && right <= high) {
if (arr[left] < arr[right]) {
temp[index++] = arr[left++];
} else {
temp[index++] = arr[right++];
}
}
// 要么左边没有元素
while (left <= mid) {
temp[index++] = arr[left++];
}
// 要么右边没有元素
while (right <= high) {
temp[index++] = arr[right++];
}
// 重新赋值给arr对应的区间。
for (int i = 0; i < temp.length; i++) {
arr[low + i] = temp[i];
}
}
}
6.快速排序(Quick Sort)
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists),本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
其基本思想是:通过一趟排序将待排的记录分割成独立的两个部分,其中一部分的关键字均比另一部分的记录的关键字小,则可分别对着两部分记录继续进行排序,以达到整个序列有序的目的。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n log n) | O(n log n) | O(n2) | O(log n) | 不稳定 |
6.1 算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
6.2 复杂度分析
- 时间复杂度为O(n logn)
- 空间复杂度为O(log n)
6.3 Java代码实现以及参考练习
class QuickSort{
public static int[] QuickSortMain(int[] sourceArray) {
return quickSort(arr, 0, arr.length - 1);
}
private int[] quickSort(int[] arr, int left, int right) {
if (left < right) {
int partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
return arr;
}
private int partition(int[] arr, int left, int right) {
// 设定基准值(pivot)
int pivot = arr[left];
int index = left + 1;
for (int i = index; i <= right; i++) {
if (arr[i] < pivot) {
swap(arr, i, index);
index++;
}
}
swap(arr, left, index - 1);
return index - 1;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
7.堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
以最大堆为例,其基本思想是:将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就跟将其与堆数组的末尾元素进行交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这就会得到n个元素中的次大值。如此反复执行,就会有一个长度为n的有序序列。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
7.1 算法步骤
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
7.2 复杂度分析
- 时间复杂度为O(n logn)
- 空间复杂度为O(1)
7.3 Java代码实现以及参考练习
LC 215. 数组中的第K个最大元素
LC 剑指Offer 40.最小的k个数
class HeapSort{
public static int[] HeapSortMain(int[] arr) {
int len = arr.length;
buildMaxHeap(arr, len); // 将数组整理成堆
for (int i = len - 1; i > 0; i--) {
swap(arr, 0, i); // 把堆顶元素(当前最大)交换到数组末尾
len--; // 逐步减少堆有序的部分
heapify(arr, 0, len); // 下标 0 位置下沉操作,使得区间 [0, i] 堆有序
}
return arr;
}
//将数组整理成堆(堆有序)
private void buildMaxHeap(int[] arr, int len) {
for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i, len);
}
}
// i为当前下沉元素的下标
private void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
8.计数排序(Counting Sort)
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
8.1 算法步骤
- 找出原数组中元素值最大的,记为max
- 创建一个新数组count,其长度是max加1,其元素默认值都为0。
- 遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。
- 创建结果数组result,起始索引index。
- 遍历count数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result数组中去,每处理一次,count中的该元素值减1,直到该元素值不大于0,依次处理count中剩下的元素。
- 返回结果数组result。
8.2 复杂度分析
- 时间复杂度为O(n+k)
- 空间复杂度为O(k)
8.3 Java代码实现以及参考练习
class CountingSort{
public static int[] countingSortMain(int[] arr) {
int maxValue = getMaxValue(arr);
return countingSort(arr, maxValue);
}
private int getMaxValue(int[] arr){
int maxValue = arr[0];c
for(int value : arr){
if(maxValue < value){
maxValue = value;
}
}
return maxValue;
}
private int[] countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int value : arr) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
}
9. 桶排序(Bucket Sort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
在额外空间充足的情况下,尽量增大桶的数量
使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
优点:实现简单,在数据量小的情况下性能良好,是稳定的排序算法
缺点:严重依赖于额外的存储空间
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n+k) | O(n+k) | O(n2) | O(n+k) | 稳定 |
9.1 算法步骤
- 计算最大值和最小值
- 根据最计算出来最大、小值,来分配桶的个数(桶的个数以可能将数据平均分配到每个桶为准)
- 每个桶内排序
- 桶和桶之间完成合并
9.2 复杂度分析
- 时间复杂度为O(n+k)
- 空间复杂度为O(n+k)
9.3 Java代码实现以及参考练习
class BucketSort{
public static int[] BucketSortMain(int[] arr) {
if (arr.length == 0) {
return arr;
}
// 计算最大值与最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
// 计算桶的数量
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
// 利用映射函数将数据分配到各个桶中
// 将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
// 对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
// 将桶中的元素赋值到原序列
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
return arr;
}
}
10.基数排序(Radix Sort)
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。所有对于字符串和文字排序不适合。
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
O(n x k) | O(n x k) | O(n x k) | O(n + k) | 稳定 |
10.1 算法步骤
实现:将所有待比较数值(自然数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的两种方式:
- 高位优先,又称为最有效键(MSD),它的比较方向是由右至左;
- 低位优先,又称为最无效键(LSD),它的比较方向是由左至右
10.2 复杂度分析
- 时间复杂度为O(n x k)
- 空间复杂度为O(n + k)
10.3 Java代码实现以及参考练习
// MSD 只考虑正整数的情况下的代码
class RadixSort{
public static int[] radixSortMain(int[] arr) {
// 高位优先法 MSD
//待排序列最大值
int max = arr[0];
int exp;//指数
//计算最大值
for (int anArr : arr) {
if (anArr > max) {
max = anArr;
}
}
//从个位开始,对数组进行排序
for (exp = 1; max / exp > 0; exp *= 10) {
//存储待排元素的临时数组
int[] temp = new int[arr.length];
//分桶个数
int[] buckets = new int[10];
//将数据出现的次数存储在buckets中
for (int value : arr) {
//(value / exp) % 10 :value的最底位(个位)
buckets[(value / exp) % 10]++;
}
//更改buckets[i],
for (int i = 1; i < 10; i++) {
buckets[i] += buckets[i - 1];
}
//将数据存储到临时数组temp中
for (int i = arr.length - 1; i >= 0; i--) {
temp[buckets[(arr[i] / exp) % 10] - 1] = arr[i];
buckets[(arr[i] / exp) % 10]--;
}
//将有序元素temp赋给arr
System.arraycopy(temp, 0, arr, 0, arr.length);
return arr;
}
}
}