数据结构与算法知识点总结(4)各类排序算法

0.先言

  排序算法是数据结构与算法中最基本的算法之一。

  排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:、

0.1 名词解释

  • n:数据规模
  • k:"桶"的个数
  • In-place:占用常数内存,不占用额外内存
  • Out-place:占用额外内存
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同

1. 插入排序

1.1 直接插入排序

  直接插入排序的特点:

  • 时空效率: 时间复杂度为O(n^2),空间复杂度为O(1)。最好情况下是元素基本有序,此时每插入一个元素,只需比较几次而无需移动,时间复杂度为O(n)

  • 稳定性: 保证相等元素的插入相对位置不会变化,稳定排序

    void insertion_sort_int(int *arr,int n) {
        for(int i = 1;i < n;i++) {
            int key = arr[i];
            int j = i - 1;
            for(; j >= 0 && arr[j] > key;j--) {
                arr[j + 1] = arr[j];
            }
            arr[j + 1] = key;
        }
    }

1.2 折半插入排序

  在查找插入位置时改为折半查找,即为折半插入排序。

void binary_insertion_sort_int(int *arr,int n) {
    for(int i = 1;i < n;i++) {
        int key = arr[i];
        int left = 0;
        int right = i - 1;
        while(left <= right) {
            int mid = (left + right) / 2;
            if(key < arr[mid]) 
                right = mid - 1;
             else
                 left = mid + 1;
        }
        for(int j = i - 1; j >= left; j--)
            arr[j + 1] = arr[j];
        arr[left] = key;
    }
}

1.3 希尔排序

  首先取长度的一半作为增量的步长d1,把表中全部记录分成d1个组,把步长隔d1的记录放在同一组中再进行直接插入排序;然后再取d1的一半作为步长,重复插入排序操作。

  • 一般默认n在某个特定范围时,希尔排序的时间复杂度为O(n^1.3)

  • 稳定性: 当相同关键字映射到不同的子表中,可能会改变相对次序,不稳定排序。例如(3,2,2)就会改变2的相对次序

void shell_sort_int(int *arr,int n) {
    for(int dk = n / 2; dk >= 1; dk >>= 1) {
        for(int i = dk; i < n;i++) {
            int key = arr[i];
            int j = i - dk;
            for(; j >=0 && arr[j] > key; j -= dk) {
                arr[j + dk] = arr[j];
            }
            arr[j + dk] = key;
        }
    }
}

2. 归并排序

2.1 二路归并排序

  二路归并排序是分治法的应用,模式如下:

  • 分解: n个元素分解成两个n/2的子序列
  • 解决: 用归并排序对两个子序列进行递归地排序
  • 合并: 合并两个有序的子序列得到排序结果

  关于两个有序列表之间的合并,只需要复制到辅助数组中,根据大小关系两两比较再放入原始数组中,这种合并方法有很多变式。二路归并排序的特点如下:

  • 时间效率: 有lgn趟归并,每次归并时间为O(n),则时间复杂度为O(nlgn)

  • 空间效率: merge操作需要辅助空间O(n),建议在merge操作外分配一个大的数组(注意也有O(1)的合并算法)

  • 稳定性: 是稳定排序,不改变相同关键字记录的相对次序

void merge(int *arr,int *help,int left,int mid,int right) {
    for(int i = left; i <= right;i++)
        help[i] = arr[i]; //把A中元素复制到B中,借助B处理
    int i = left;
    int j = mid + 1;
    int k = left;
    while(i <= mid && j <= right) { //记录有多少个共同的
        if(help[i] < help[j]) 
            arr[k++] = help[i++]; //较小值复制到A中
        else 
            arr[k++] = help[j++];
    }
    while(i <= mid)   arr[k++] = help[i++]; //某表未检测完直接复制
    while(j <= right) arr[k++] = help[j++];
}


void merge_sort_int(int *arr,int *help,int left,int right) {
    if(left < right) {
        int mid = ((right - left) >> 1) + left;
        merge_sort_int(arr,help,left,mid);
        merge_sort_int(arr,help,mid + 1,right);
        merge(arr,help,left,mid,right);
    }
}

2.2 原地归并排序

  原地归并排序不需要辅助数组就可以归并,关键在于merge函数。思路是:

  • 遍历i找到第一个arr[i] > arr[j](start = j),确定i的位置。也就是说在这之前的元素都小于从j开始的元素
  • 遍历j找到第一个元素arr[j] > arr[i](end = j),确定j的位置,则说明在这之前的元素小于从i开始的元素

  上面操作说明第二个序列(start,end-1)的元素都应该在i之前-采用循环移位的办法移到i前面。
  例如0 1 5 6 9 | 2 3 4 7 8:

  • 第一次遍历确定了5和7,然后就把2 3 4循环移位到5 6 9前面,
  • i再从5的新位置开始作为第一个序列,j的位置是第二个序列开始
/*三个辅助函数*/
void swap(int *a,int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

/*长度为n的数组翻转*/
void reverse(int *arr,int n) {
    int i = 0;
    int j = n - 1;
    while(i < j) {
        swap(&arr[i],&arr[j]);
        i++;
        j--;
    }
}

/**
 * 将含有n个元素的数组左循环移位i位
 */
void rotation_left(int *arr,int n,int i) {
    reverse(arr,i);
    reverse(arr + i,n - i);
    reverse(arr,n);
}

void merge_inplace(int  *arr,int left,int mid,int right) {
    int i = left,j = mid + 1, k = right;
    while(i < j && j <= k) {
        while(i < j && arr[i] <= arr[j]) i++;
        int step = 0;
        while(j <= k && arr[j] <= arr[i]) {
            j++;
            step++;
        }
        //arr+i为子数组,j-i表示子数组元素个数,j-i-step表示左循环移位的位数(把前面的元素移到后面去)
        rotation_left(arr + i,j- i,j - i - step);
    }
}


void merge_sort_inplace(int *arr,int left,int right) {
    if(left < right) {
        int mid = (left + right) / 2;
        merge_sort_inplace(arr,left,mid);
        merge_sort_inplace(arr,mid+1,right);
        merge_inplace(arr,left,mid,right);
    }
}

3. 交换排序

3.1 冒泡排序

  冒泡排序总共要进行n-1趟遍历,每次都能确定一个元素最终的位置(剩余序列中的最值)。在一个序列中,正向遍历能确定一个最大值,反向遍历确定一个最小值,每次序列减少一个元素。它的特点如下:

  • 时空效率: 平均时间复杂度为O(n^2),空间复杂度为O(1),产生的有序子序列一定是全局有序的

  • 稳定性: 稳定的排序方法

void bubble_sort_int(int *arr,int n) {
    for(int i = 0; i < n - 1; i++) { //n-1趟冒泡,每次确定一个元素,每次确定小值元素
        bool flag = false;
        for(int j = n- 1; j > i; j--) {
            if(arr[j - 1] > arr[j]) { //有逆序
                swap(&arr[j - 1],&arr[j]);
                flag = true; //发生交换
            }
        }
        if(flag == false)
            return ; //表明本次遍历没有交换,则表已经有序
    }
}

3.2 快速排序

  快速排序的核心在于划分操作,它的性能也取决于划分操作的好坏,快排是所有内部排序算法中平均性能最好的。它的特点如下:

  • 时间效率: 快排的运行时间与划分是否对称有关,平均时间复杂度为O(nlgn),最坏为O(n^2)

  • 为了提高算法效率: 在元素基本有序时采取直接插入排序(STL就这么干,给定一个阈值)

  • 为了保证划分的平衡性,例如三数取中位数或者随机选择pivot元素甚至Tukey ninther选取策略,这样使得最坏情况在实际情形中不太可能发生

  • 空间效率: 采用递归工作栈,空间复杂度最好情况下为O(lgn),最坏为O(n)

  • 稳定性: 在划分算法中,若右侧有两个相同的元素,在小于pivot时交换到左侧相对位置就会发生变化,表明快排是一种不稳定排序,但每趟排序能将一个元素位置确定下来(pivot)。注意使用有些划分算法能够保证局部稳定性

  快速排序的主框架如下:

void quick_sort_int(int *arr,int left,int right) {
    if(left < right) {
        int pos = partition1(arr,left,right); //划分
        quick_sort_int(arr,left,pos - 1);
        quick_sort_int(arr,pos + 1,right);
    }
}

3.2.1 首尾指针向中间扫描法

  两指针分别从首尾向中间扫描。这里主要考虑的是选取pivot的策略,可能是首元素或尾元素,甚至是三数取中或者随机取某个数。思路是将数据以pivot作划分,右侧小于pivot移到左侧,左侧大于pivot移到右侧

  此方法可以用来确定第k大或第k小的元素。

int partition(int *arr,int left,int right) {
    int pivot = arr[left];
    while(left < right) {
        while(left < right && pivot <= arr[right])
            right--;
        arr[left] = arr[right]; //比pivot值小的移到左端
        while(left < right && pivot >= arr[left])
            left++;
        arr[right] = arr[left]; //比pivot值大的移到右端
    }
    arr[left] = pivot; //left == right
    return left;
}

  其实还有一种交换策略: 把左右端不满足条件的元素交换,最后循环终止的元素应该与开始元素进行交换。这种方法结合前后两个策略: 既双向遍历又使用了交换操作。

 

int partition1(int *arr,int left,int right) {
    int start = left;
    int pivot = arr[left];
    while(left < right) {
        while(left < right && pivot <= arr[right])
            right--;
        while(left < right && pivot >= arr[left])
            left++;
        swap(&arr[left],&arr[right]); 
    }
    swap(&arr[start],&arr[left]);
    return left;
}

3.2.2 一前一后指针向后扫描法

  采用单向遍历: 两指针索引一前一后逐步向后扫描。它的策略是: 记录一个last指针,用于保存小于或等于pivot的元素。从左往右扫描,每遇见一个元素小于等于pivot即将它存储于last指针里,所以一趟划分过后last之前的元素(包括last,这取决于last的初始化)小于等于pivot。

  可以看出该划分算法的特点: 从前向后遍历,last记录的是小于等于pivot的元素,这些元素相对顺序不变。若要大于等于pivot的元素相对顺序不变,可从后向前遍历,pivot取首元素。

int partition2(int *arr,int left,int right) {
    int pivot = arr[right];
    int last = left -1;
    for(int i = left; i < right; i++) {
        if(arr[i] <= pivot) {
            ++last;
            swap(&arr[last],&arr[i]);
        }
    }
    swap(&arr[last + 1],&arr[right]);
    return last + 1;
}

  如果pivot不方便取,例如Dijkstra提出的荷兰三色旗问题要保证0,1,2三者有序,使用单纯的单向遍历会存在0和1顺序乱的情况,并且元素的重复次数比较多,所以Dijkstra提出了一种简单快速的三向划分法。

3.2.3 三向划分法

  Dijkstra三向快速切分: 用于处理有大量重复元素的情形,减少递归时重复元素的比较的次数。即遍历数组一次,维护三个指针lt、cur、gt

  • lt使得arr[0..lt-1]的元素小于v
  • gt使得arr[gt+1..n-1]的元素大于v
  • cur使得arr[lt..i-1]的元素等于v,arr[i..gt]的元素仍不确定

  这和荷兰三色旗是类似的问题,思路比较简单。要学会掌握用一个工作指针结合两个边界指针进行一次线性遍历,同样有很多变式。完整的三向划分快速排序代码如下:

void quick_sort_threeway(int *arr,int left,int right) {
    if(left < right) {
        int lt = left;
        int cur = left;
        int gt = right;
        int pivot = arr[left];
        while(cur <= gt) {
            if(arr[cur] == pivot) {
                cur++;
            } else if(arr[cur] < pivot) {
                swap(&arr[cur],&arr[lt]);
                lt++;
                cur++;
            } else {
                swap(&arr[cur],&arr[gt]);
                gt--;
            }
        } //cur > gt则退出,直接形成了left..lt-1 lt..gt gt+1..right三个区间
        quick_sort_threeway(arr,left,lt - 1);
        quick_sort_threeway(arr,gt + 1,right);
    }
    
}

  不过这种方法唯一的缺点就是在数组中重复元素不多的情况下比标准的二分法多使用了很多次交换。90年代,John Bently等人用一个聪明的方法解决了此问题,使得三向切分的快排比一般的排序方法都要快。

3.2.4 快速三向划分法

  Bently用重复元素放置于子数组两端的方式实现了一个信息量最优的算法,这里额外的交换只用于和切分的pivot元素相等的元素,上面的额外交换是用于切分不相等的元素。

  思路如下:

  • 维护两个索引p、q,使得arr[lo..p-1]和arr[q+1..hi]的元素都和 a[lo]相等(记为v)
  • 使用两个索引i、j,使得a[p..i-1]小于v,a[j+1..q]大于v。
  • 若在前面遇到等于v的元素与p交换,后面的与q交换(类似操作)

  如图:

  它的代码如下:

void quick_sort_threeway_fast(int *arr,int left,int right) {
    if(left < right) {
        int p = left , q = right + 1; 
        int pivot = arr[left];
        int i = left , j = right + 1;
        while(true) {
            while(arr[++i] < pivot) 
                if(i == right) 
                    break;
            while(arr[--j] > pivot) 
                if(j == left)
                    break;

            if(i == j && arr[i] == pivot)
                swap(&arr[++p],&arr[i]);
            if(i >= j) break;

            swap(&arr[i],&arr[j]);
            if(arr[i] == pivot)
                swap(&arr[++p],&arr[i]);
            if(arr[j] == pivot)
                swap(&arr[--q],&arr[j]);
        }
        i = j + 1;
        for(int k = left; k <= p; k++) swap(&arr[k],&arr[j--]);
        for(int k = right; k >= q;k--) swap(&arr[k],&arr[i++]);
        quick_sort_threeway_fast(arr,left,j);
        quick_sort_threeway_fast(arr,i,right);
    }
}

3.3 最优化的排序算法

  在元素个数比较小的时候使用直接插入排序,元素个数较多的适合主要在于pivot元素的选取策略。代码如下:

/**
 * 三数取中策略: 返回数组的下标
 */
int median3(int *arr,int i,int j,int k) {
    if(arr[i] < arr[j]) {
        if(arr[j] < arr[k])
            return j;
        else {
            return (arr[i] < arr[k])? k : i;
        }
    } else {
        if(arr[k] < arr[j])
            return j;
        else {
            return (arr[k] < arr[i])? k : i;
        }
    }
}

void optimal_sort_int(int *arr,int left,int right) {
    int n = right - left + 1;
    if(n <= THRESHOLD) {
        insertion_sort_int(arr,n);
    } else if(n <= 500) { //用三数取中排序
        int pos = median3(arr,left, left + n / 2,right);
        swap(&arr[pos],&arr[left]);
    } else {//采取Tukey ninther 作为pivot元素
        int eighth = n / 8;
        int mid = left + n / 2;
        int m1 = median3(arr,left,left + eighth,left + eighth * 2);
        int m2 = median3(arr,mid - eighth,mid,mid + eighth);
        int m3 = median3(arr,right - eighth * 2,right - eighth,right);
        int ninther = median3(arr,m1,m2,m3);
        swap(&arr[ninther],&arr[left]);
    }
    quick_sort_threeway_fast(arr,left,right);
}

4. 选择排序

4.1 简单选择排序

  选择排序的基本思路: 每一趟要在后面的元素中确定一个最值元素,重复n - 1趟。它的特点如下:

  • 时间效率: 注意元素的比较次数与初始序列无关,始终要比较n(n-1)/2,时间复杂度为O(n^2)
  • 空间效率为O(1)
  • 稳定性:不稳定排序,例如6 8 6 5
void selection_sort_int(int *arr,int n) {
    int min;
    for(int i = 0; i < n - 1;i++) {//n-1趟选择排序,每次选择一个最小值
        min = i;
        for(int j = i + 1; j < n;j++) {
            if(arr[min] < arr[j]) {
                min = j; //更新最小元素位置
            }
        }
        if(min != i) swap(&arr[i],&arr[min]); //更新到的最小值与i位置交换
    }
}

4.2 堆排序和优先级队列

  堆排序是一种树形选择排序,利用完全二叉树中父节点和子节点的关系选择最值的元素。排序的步骤如下:

  • 先构造大根堆(这也是一个反复向下调整满足最大堆性质的操作)
  • 然后再把堆顶元素与堆底元素交换,此时根结点不再满足堆的性质,堆顶元素向下调整重复操作,直到堆中剩一个元素为止,要进行n-1趟交换和调整操作

  堆排序的特点是:

  • 时空效率: 在最好、平均和最坏情况下,堆排序的时间复杂度均为O(nlgn);空间复杂度: O(1)

  • 稳定性: 不稳定排序

  堆排序的主程序如下(注意0号不存储元素,实际存储从1开始,数组长度为len + 1):

void heap_sort_int(int *arr,int len) {
    build_max_heap(arr,len);
    for(int i = len; i > 1;i--) {
        swap(&arr[i],&arr[1]); //和堆顶元素交换,并输出堆顶元素
        sink_down(arr,1,i - 1); //注意还剩余i-1个元素调整堆
    }
}

4.2.1 堆调整操作

  A 下标为k的堆的自顶向下操作

  这种操作是为了保持根为下标k的元素的子树满足最大堆性质。如果某个节点比它们的两个子节点或之一更小,则以该节点为根的子树不满足最大堆性质。

  调整思路: 把更小的元素由上至下下沉,以类似的方式维持其子节点的堆状态性质直到某子节点元素满足最大堆的状态。向下调整的时间与树高有关为O(h)

  此操作用于构造堆,堆排序和删除操作(delMax),代码如下:

void sink_down(int *arr,int k,int len) {
    while(2 * k <= len) {
        int j = 2 * k;
        if(j <len && arr[j] < arr[j + 1]) j++; //取更大的元素
        if(arr[k] > arr[j]) break; //根元素大于子节点,则满足最大堆性质无需调整
        swap(&arr[k],&arr[j]);
        k = j;
    }
}

B 下标为k的堆自底向上操作

  这种操作是因为该节点的大小比其父节点更大,则需要进行向上调整,注意终止条件是k=1且父节点更大。

  该操作用于在堆底插入新元素

void swim_up(int *arr,int k) {
    while(k > 1 && arr[k] > arr[k / 2]) {
        swap(&arr[k],&arr[k / 2]);
        k = k / 2;
    }
}

  总结起来堆和优先级队列的插入、删除和取最值还是比较简单的,关键还是在于这两个堆调整操作,代码量不大,但需要理解其运行逻辑。

4.2.2 构造堆操作

  以无序数组自底向上构造出一个最大堆,时间复杂度为O(n),实际存储从下标1开始,数组长度为len + 1。从n=len/2开始进行自顶向下调整操作

void build_max_heap(int *arr,int len) {
    for(int i = len / 2; i >= 1;i--)
        sink_down(arr,i,len);
}

  大根堆一般可用于求海量数据中最小的k个数: 即先读取k个元素构造最大堆,再依次读入数据。若当前数据比堆顶小,则替换堆顶;若当前数据比较大,不可能是最小的k个数 。对应地求最大的k个数一般用小根堆。

  如下分别是用快排的划分算法和堆的性质取得最小的k个数

void print(int *arr,int left,int right) {
    for(int i = left; i<= right;i++)
        printf("%d ",arr[i]);
    printf("\n");
}

/*基于partition算法取最小的k个数*/
void getleastk(int *arr,int n,int k) {
     int left = 0;
    int right = n - 1;
    int pos = partition(arr,left,right);
    while(pos != k - 1) {
        if(pos > k - 1) {
            right = pos - 1;
            pos = partition(arr,left,right);
        } else {
            left = pos + 1;
            pos = partition(arr,left,right);
        }
    }
    print(arr,0,k-1);
}

/*基于最大堆求最小的k个数*/
void getmink(int *arr,int n,int k) {
    int b[k+1];
    for(int i = 1; i <= k;i++) {
            b[i] = arr[i - 1];        
    }

    build_max_heap(b,k);
    for(int i = k; i < n;i++) {
        if(arr[i] > b[1])
            continue;
        else {
            b[1] = arr[i];
            sink_down(b,1,k);
        }
    }
    heap_sort_int(b,k);
    print(b,1,k);
}

5. 线性时间排序

5.1 计数排序

  计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

  当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

  由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

  通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。

   算法的步骤如下:

  • (1)找出待排序的数组中最大和最小的元素
  • (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  • (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
public class CountingSort implements IArraySort {

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        int maxValue = getMaxValue(arr);

        return countingSort(arr, 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;
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }

}

5.2 基数排序

  基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

  基数排序有两种方法:

  这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  1. 基数排序:根据键值的每位数字来分配桶;
  2. 计数排序:每个桶只存储单一键值;
  3. 桶排序:每个桶存储一定范围的数值;
public class RadixSort implements IArraySort {

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        int maxDigit = getMaxDigit(arr);
        return radixSort(arr, maxDigit);
    }

    /**
     * 获取最高位数
     */
    private int getMaxDigit(int[] arr) {
        int maxValue = getMaxValue(arr);
        return getNumLenght(maxValue);
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }

    protected int getNumLenght(long num) {
        if (num == 0) {
            return 1;
        }
        int lenght = 0;
        for (long temp = num; temp != 0; temp /= 10) {
            lenght++;
        }
        return lenght;
    }

    private int[] radixSort(int[] arr, int maxDigit) {
        int mod = 10;
        int dev = 1;

        for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
            // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
            int[][] counter = new int[mod * 2][0];

            for (int j = 0; j < arr.length; j++) {
                int bucket = ((arr[j] % mod) / dev) + mod;
                counter[bucket] = arrayAppend(counter[bucket], arr[j]);
            }

            int pos = 0;
            for (int[] bucket : counter) {
                for (int value : bucket) {
                    arr[pos++] = value;
                }
            }
        }

        return arr;
    }

    /**
     * 自动扩容,并保存数据
     *
     * @param arr
     * @param value
     */
    private int[] arrayAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }
}

5.3 桶排序

  桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

  同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

public class BucketSort implements IArraySort {

    private static final InsertSort insertSort = new InsertSort();

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        return bucketSort(arr, 5);
    }

    private int[] bucketSort(int[] arr, int bucketSize) throws Exception {
        if (arr.length == 0) {
            return arr;
        }

        int minValue = arr[0];
        int maxValue = arr[0];
        for (int value : arr) {
            if (value < minValue) {
                minValue = value;
            } else if (value > maxValue) {
                maxValue = value;
            }
        }

        int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
        int[][] buckets = new int[bucketCount][0];

        // 利用映射函数将数据分配到各个桶中
        for (int i = 0; i < arr.length; i++) {
            int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
            buckets[index] = arrAppend(buckets[index], arr[i]);
        }

        int arrIndex = 0;
        for (int[] bucket : buckets) {
            if (bucket.length <= 0) {
                continue;
            }
            // 对每个桶进行排序,这里使用了插入排序
            bucket = insertSort.sort(bucket);
            for (int value : bucket) {
                arr[arrIndex++] = value;
            }
        }

        return arr;
    }

    /**
     * 自动扩容,并保存数据
     *
     * @param arr
     * @param value
     */
    private int[] arrAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }

}
posted @ 2022-04-18 13:08  LyAsano  阅读(566)  评论(0编辑  收藏  举报