依赖数组特性的几种非比较排序算法

前言:

  前面所讲的排序算法基本都是需要进行两个数依次比较,这种两个数依次比较的算法不依赖于数组重元素的特性并且有下界Ω(nlogn)。换句话说就是使用比较排序算法最快的时间消耗没法小于这个界。那么是不是我们永远没法跨越这个梗呢?答案当然不是,当数组中的元素有一定的特点的时候,我们就可以利用这个特定,以实现排序算法的时间消耗与n呈线性的关系。

特性一:数组中所有元素正负性一致并且他们绝对值都小于某一个数。

  当数组中所有元素都为正数或者都为负数的时候其实比较的算法是一致。这里我们假设所有元素都是非负。关于这个特性我们的思路灵感可能来自于统计一段文字中每个字母出现的次数。我们可以假设数组中所有元素都小于k。那么我们可以建立一个长度为k的数组,通过遍历要排序的数组,我们可以知道元数组中特定值的元素的个数。更进一步的,完成第一步之后我们可以知道原数组中小于等于某一元素的个数。既然我们知道了小于该元素的个数,就很简单的能得到该元素应该在数组中的位置。  这种排序算法叫做计数排序(Counting Sort)。源代码如下:

 1 public static void main(String[] args) {
 2         int[] arr = { 1, 12, 11, 4, 5, 7, 3, 1, 23, 56, 34, 76, 25, 76 };
 3         countingSort(arr, 80);
 4     }
 5
 6     private static void countingSort(int[] arr, int upBound/** 数组中元素的上界 */
 7     ) {
 8         // 新建一个数组,该数组用来存储arr中所有值出现的次数(如果不给数组中的元素赋值,默认每个值都为0)
 9         int[] valueCountArr = new int[upBound];
10
11         // 计算arr中对应值出现的次数
12         for (int i = 0; i < arr.length; i++) {
13             valueCountArr[arr[i]] = valueCountArr[arr[i]] + 1;
14         }
15
16         // 使valueCountArr[i]中存储的为原数组中小于等于i的元素的个数
17         for (int i = 1; i < upBound; i++) {
18             valueCountArr[i] += valueCountArr[i - 1];
19         }
20
21         // 新建一个数组用来存放排序完成之后的值
22         int[] sortedArr = new int[arr.length];
23         for (int i = 0; i < arr.length; i++) {
24             sortedArr[valueCountArr[arr[i]] - 1] = arr[i]; // 如果小于等于arr[i]的值的个数为j,那么arr[i]在排序好的数组中的位置应该在arr[j-1]
25             valueCountArr[arr[i]] = valueCountArr[arr[i]] - 1; // 此时小于等于特定值的计数应该减一(如果存在同样大小的元素,需要把计数减一,不然两个值的位置会互相覆盖)
26         }
27
28         System.out.println(Arrays.toString(sortedArr));
29     }

CountingSort

特性二:数组中的元素的位数都一样且数组中元素的正负性一致。

  同特性一,我们假设所有元素都是非负的。这一特性可供我们利用的一点就是从个位数开始分别比较每一位的值。假设每一位的值有上界k(其实k最大为10)。我们可以假设共有n个元素,每个元素都有d位,每位数字都小于k。我们其实可以针对每一位都进行计数排序。这样每一次计数排序的耗时为:θ(n+k),共有d位,所以总共的耗时为:θ(d(n+k))。

因为d、k都是一个常量。所以这其实也是关于n的线性排序。这种排序算法叫做基数排序(RadixSort)。源代码如下:

 1 public static void main(String[] args) {
 2         int[] arr = { 123, 111, 213, 110, 990, 212, 345, 541, 246, 798, 555 };
 3         radixSort(arr, 3);
 4     }
 5
 6     private static void radixSort(int[] arr, int digit/** 每一个数字的位数 */
 7     ) {
 8         int[] eachDigitArr = new int[arr.length];
 9         for (int i = 1; i <= digit; i++) {
10             for (int j = 0; j < arr.length; j++) {
11                 eachDigitArr[j] = arr[j] % (int) Math.pow(10, i)
12                         / (int) Math.pow(10, i - 1);
13             }
14             sortDigit(arr, eachDigitArr);
15         }
16         System.out.println(Arrays.toString(arr));
17     }
18
19     private static void sortDigit(int[] arr, int[] eachDigitArr) {
20
21         // 因为每一位数都小于10,所以可以设置上界为10的计数排序
22         int[] valueCountArr = new int[10];
23         int[] indexArr = new int[arr.length]; // 还需要定义一个数组用来存放排序之后每一个位置对应排序之前的数组中元素的下标
24         for (int i = 0; i < arr.length; i++) {
25             indexArr[i] = i;
26         }
27
28         for (int i = 0; i < eachDigitArr.length; i++) {
29             valueCountArr[eachDigitArr[i]] += 1;
30         }
31
32         for (int i = 1; i < 10; i++) {
33             valueCountArr[i] = valueCountArr[i] + valueCountArr[i - 1];
34         }
35
36         // 在计数排序的过程更新排序后对应位置的元素在原数组中的下标(此时应该从数组末尾进行遍历,以保证前面的排序对这次排序的限定性)
37         for (int i = eachDigitArr.length - 1; i >= 0; i--) {
38             indexArr[valueCountArr[eachDigitArr[i]] - 1] = i;
39             valueCountArr[eachDigitArr[i]] -= 1;
40         }
41
42         int[] resultArr = new int[arr.length];
43         for (int i = 0; i < arr.length; i++) {
44             resultArr[i] = arr[indexArr[i]];
45         }
46
47         System.arraycopy(resultArr, 0, arr, 0, arr.length);
48     }

RadixSort

特性三:数组中的元素均匀的分布在一定的范围内。

  这个特性排序算法的灵感来自于HashCode的生成规则以及HashMap的存储结构。该算法的原理大致是:维护一个数组,数组中的每一个元素相当于一个列表。每个列表存储了拥有相同特性的元素。假设被维护的数组为arr,arr[i]和arr[j]为维护数组中的两个元素。那么对于任意i、j,如果i<j。那么arr[i]中的所有元素都小于arr[j]中的所有元素。这样其实对于任意元素,如果该元素属于arr[i],那么其实只要用插入排序算法插入arr[i]中的元素列表即可。最后将维护的数组每一个下标元素中的元素列表拼接起来即为最终结果。这种算法称作桶排序(BucketSort)。源代码如下:

 1 public static void main(String[] args) {
 2         int[] arr = { 1, 12, 11, 4, 5, 7, 3, 1, 23, 56, 34, 76, 25, 76 };
 3         bucketSort(arr);
 4     }
 5
 6     private static void bucketSort(int[] arr) {
 7         // 观察数组可以认为数组均匀的分布在(0 , 100)之间,我们可以将数组中的元素按照除以10所得的余数不同来进行分组,这样就一共有10组。
 8         // 建立一个二维数组来进行相同特性的元素划分维护
 9         int[][] bucketArr = new int[10][10];
10         for (int i = 0; i < 10; i++) {
11             for (int j = 0; j < 10; j++) {
12                 bucketArr[i][j] = 100;
13             }
14         }
15
16         for (int i = 0; i < 10; i++) {
17             insert2Arr(bucketArr[arr[i] / 10], arr[i]);
18         }
19
20         int k = 0;
21         for (int i = 0; i < 10; i++) {
22             for (int j = 0; j < bucketArr[i].length; j++) {
23                 if (bucketArr[i][j] < 100) {
24                     arr[k++] = bucketArr[i][j];
25                 }else {
26                     continue;
27                 }
28             }
29         }
30         System.out.println(Arrays.toString(arr));
31     }
32
33     private static void insert2Arr(int[] arr, int value) {
34         arr[arr.length - 1] = value;
35         for (int i = arr.length - 1; i > 0; i--) {
36             if (arr[i] < arr[i - 1]) {
37                 swap(arr, i, i - 1);
38             } else {
39                 break;
40             }
41         }
42     }
43
44     private static void swap(int[] arr, int index1, int index2) {
45         int temp = arr[index1];
46         arr[index1] = arr[index2];
47         arr[index2] = temp;
48     }

BucketSort

   可以看到该算法最后合并的耗时为:θ(n)。中间针对每一个桶进行排序我们可以假设每一次进行桶的某一个列表进行插入排序的时候列表的元素有n个。那么有公式:

T(n) = θ(n) + ΣΟ(n)2  其中i的取值为1到n-1。第二项的取值不好计算,我们可以定义指示器变量 Xij = I(表示a[j]落在桶i中)。其中i=0, 1, 2, ... n-1;j=0, 1, 2... n-1(桶的长度最多和数组元素个数一直)。 所以ni = Σ(Xij ) (其中j为0, 1, 2, ... , n-1)。对刚刚的时间消耗公式同时取期望得:

E[T(n)] = E[θ(n) + ΣΟ(n)2] = θ(n) + ΣΟ(E[ni ]2)。 所以有 E[n]= E[ Σ(Xij )2] 。后面是一个多项式乘以多项式:E[ Σ(Xij )2]  = E[ ΣXij2 + ΣXijΣXik] = ΣE[Xij2] + ΣΣE[XijXik](其中j和k的取值都是0, 1, 2, .. n-1且k!=j)。指示器随机变量Xij为1的概率为1/n,其他情况下该指示器变量的值为0。易得: E[Xij2] = 1*1/n + 0*(1- 1/n) = 1/n。

又当k!=j的时候Xij、Xik 是相互独立的。所以有E[XijXik] = 1/n * 1/n = 1/n2。进而得到:  E[n]2  =  Σ(1/n) + ΣΣ(1/n2) = 2 - 1/n。对消耗的时间去掉取期望之后得到:

T(n) = θ(n) + n*Ο(2- 1/n) = θ(n)。其实即使数组中的元素不是均匀分布,桶排列也可以得到关于n的线性时间消耗。

总结

  以上的三种排序突破了数组比较排序的下界。但是他们依赖于数组的特性,而且暂用的空间也比堆排序和数组排序这种原数组内部进行替换的排序大。在实际应用中应该根据需要进行特定的算法选择。

时间: 2024-09-30 05:16:59

依赖数组特性的几种非比较排序算法的相关文章

java几种常见的排序算法总结

[java] view plain copy /*************几种常见的排序算法总结***************************/ package paixu; public class PaiXu { final int MAX=20; int num[]=new int[MAX]; { System.out.print("生成的随机数组是:"); for(int i=0;i<20;i++){ num[i]=(int)(Math.random()*100)

归并排序是一种有效的排序算法

gamefrye 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用. 将已有序的子序列合并,得到完全有序的序列:即先使每个子序列有序,再使子序列段间有序.若将两个有序表合并成一个有序表,称为二路归并. 归并排序的基本思想 将待排序序列R[0...n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表:将这些有序序列再次归并,得到n/4个长度为4的有序序列:如此反复进行下去,最后得到一

几种常用的排序算法总结

主要针对于插入排序,交换(冒泡和快速),选择,堆排序,归并这几种排序的基本原理和时间复杂度,及空间复杂度的一个总结. 一.插入排序 基本执行过程:3  5  2  7  9  8 1.从小到大:从第二个数开始,每次比较都与前边的几个数进行比较 但是从大到小,要先与前边排好序的几个数中的最大的开始进行比较即倒序比较,依次往前推. 如:5 先与3进行比较,比3大,所以直接排在3的后边为:3 5: 2要先与5进行比较,比5小,再与3比较,比3小,所以排序后为 2 3 5: 7要先与5比,比5大,所以直

视觉直观感受 7 种常用的排序算法

1. 快速排序 介绍: 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性. 步骤: 从数列中挑出一个元素,称为 "基准"(pivot), 重新排序数列,所有元

7 种常用的排序算法-视觉直观感受

7 种常用的排序算法-可视化 1. 快速排序 介绍: 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性. 步骤: 从数列中挑出一个元素,称为 “基准”(pivot),

7 种常用的排序算法直观感受

1. 快速排序 介绍: 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性. 步骤: 从数列中挑出一个元素,称为 "基准"(pivot), 重新排序数列,所有元

几种常见的排序算法

1.插入类排序 在一个已经有序的序列中,插入一个新的记录.有直接插入排序.折半插入排序.希尔排序. 插入类排序 直接插入排序 1 void InsertSort(int R[], int n) 2 { 3 int i, j; 4 int temp; 5 for (i = 1; i < n; ++i) 6 { 7 temp = R[i]; 8 j = i - 1; 9 while (j >= 0 && temp < R[j]) 10 { 11 R[j+1] = R[j];

C语言几种常用的排序算法

/* ============================================================================= 相关知识介绍(所有定义只为帮助读者理解相关概念,并非严格定义): 1.稳定排序和非稳定排序    简单地说就是所有相等的数经过某种排序方法后,仍能保持它们在排序之前的相对次序,我们就 说这种排序方法是稳定的.反之,就是非稳定的.  比如:一组数排序前是a1,a2,a3,a4,a5,其中a2=a4,经过某种排序后为a1,a2,a4,a3,

八种常用的排序算法(转)

下面要讲到的8种排序都属于内部排序,既在内存中完成,主要从理论原理方面来分析的.    插入排序 ①直接插入排序 例:六个数12 15 9 20  6 31 24 用直接插入排序,如下图: 思路: 第一步:从给出的六个数中,随便拿出一个数,比如12,形成一个有序的数据序列(一个数当然是有序的数据序列了,不看12之外的数,就当其他的数不存在): 第二步:从剩下的五个数中挑出一个数来,比如15,和刚才的12作比较,12<15,因此,放在12后面,形成数据序列12 15: 第三步:从剩下的四个数中挑出