JAVA实现八大排序+二分查找
简单介绍
排序是计算机内经常进行的一种操作,其目的是将一组**“无序”的记录序列调整为“有序”**的记录序列。
排序分为内部排序和外部排序。
若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。
反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
八大排序算法均属于内部排序。如果按照策略来分类,大致可分为:交换排序、插入排序、选择排序、归并排序和基数排序。如下图所示:
下表给出各种排序的基本性能,具体分析请参看各排序的详解:
直接插入排序
基本思想
通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了要给插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。
算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|
O(n2) | O(n2) | O(n2) | O(1) |
插入排序所需的时间取决于输入元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。
如果 比较操作 的代价比 交换操作 大的话,可以采用二分查找法来减少 比较操作 的数目。该算法可以认为是 插入排序 的一个变种,称为二分查找插入排序。
Java实现
private static void 直接排序(int[] array){
for (int compare_position = 0; compare_position < array.length - 1; compare_position++)
{
for (int compared_position = compare_position + 1; compared_position > 0; compared_position--) {
if (array[compared_position] < array[compared_position - 1]) {
int temp = array[compared_position];
array[compared_position] = array[compared_position - 1];
array[compared_position - 1] = temp;
}
}
}
}
冒泡排序
基本思想
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。**由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。**冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n2/2次, 时间复杂度为O(n2). 最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n). 平均来讲, 时间复杂度为O(n2). 由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1).
算法描述
冒泡排序算法的运作如下:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|
O(n2) | O(n) | O(n2) | O(1) |
Java实现
private static void 冒泡排序(int[] array){
for (int compare_times = 0 ; compare_times < array.length-1; compare_times++) //外层循环控制比较的次数
{
for (int position = 0 ; position < array.length-compare_times-1; position++ ) 内层循环控制到达位置
{
//前面的元素比后面大就交换
if (array[position]>array[position+1]){
int temp = array[position];
array[position]=array[position+1];
array[position+1]=temp;
}
}
}
}
快速排序
基本思想
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序的基本思想:挖坑填数+分治法。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n2),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好。
算法描述
快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:
- 从数列中挑出一个元素,称为"基准"(pivot)。
- 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|
O(nlog?n) | O(nlog?n) | O(n2) | O(1)(原地分区递归版) |
Java实现
private static void 快速排序_递归(int[] array,int low,int high){
if (low>high){
return;
}
int left_limit = low,right_limit = high,compare = array[left_limit];
while(left_limit<right_limit){
while(left_limit<right_limit&&array[right_limit]>=compare){
right_limit--;
}
array[left_limit] = array[right_limit];
while (left_limit<right_limit&&array[left_limit]<=compare){
left_limit++;
}
array[right_limit] = array[left_limit];
}
array[left_limit]=compare;
快速排序_递归(array,low,left_limit-1);
快速排序_递归(array,left_limit+1,high);
}
简单选择排序
基本思想
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n个元素的表进行排序总共进行至多 n-1 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
选择排序的简单和直观名副其实,这也造就了它”出了名的慢性子”,无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n2/2次遍历来确认一遍。即便是这样,它的排序结果也还是不稳定的。 唯一值得高兴的是,它并不耗费额外的内存空间。
算法描述
- 从未排序序列中,找到关键字最小的元素
- 如果最小元素不是未排序序列的第一个元素,将其和未排序序列第一个元素互换
- 重复1、2步,直到排序结束。
动图效果如下所示:
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|
O(n2) | O(n2) | O(n2) | O(1) |
Java实现
private static void 简单选择排序(int[] array){
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i+1; j < array.length ; j++ ){
if (array[j]<array[min]){
min = j;
}
}
if (min!=i){
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
}
}
希尔排序
基本思想
希尔排序,也称 递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是 非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一
希尔排序是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
可以看到步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。一般来说最简单的步长取值是初次取数组长度的一半为增量,之后每次再减半,直到增量为1。更好的步长序列取值可以参考维基百科。
希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。
算法描述
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|
O(nlog2 n) | O(nlog2 n) | O(nlog2 n) | O(1) |
Java实现
private static void 希尔排序(int[] array){
int length = array.length;
int h = 1;
while (h < length / 3)
h = 3 * h + 1;
for (; h >= 1; h /= 3) {
for (int i = 0; i < array.length - h; i += h) {
for (int j = i + h; j > 0; j -= h) {
if (array[j] < array[j - h]) {
int temp = array[j];
array[j] = array[j - h];
array[j - h] = temp;
}
}
}
}
}
归并排序
基本思想
归并排序是建立在归并操作上的一种有效的排序算法,1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
从效率上看,归并排序可算是排序算法中的”佼佼者”. 假设数组长度为n,那么拆分数组共需logn,, 又每步都是一个普通的合并子数组的过程, 时间复杂度为O(n), 故其综合时间复杂度为O(nlogn)。另一方面, 归并排序多次递归过程中拆分的子数组需要保存在内存空间, 其空间复杂度为O(n)。
算法描述
递归法(假设序列共有n个元素):
- 将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素;
- 将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素;
- 重复步骤2,直到所有元素排序完毕。
迭代法
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|
O(nlog?n) | O(nlog?n) | O(nlog?n) | O(n) |
Java实现
private static int[] aux;
private static void merge(int[] array,int low ,int mid, int high){
int i = low,j = mid+1;
for (int k = low; k <= high ; k++ ){
aux[k]=array[k];
}
for (int k = low; k <= high ; k++ ){
if (i>mid)
{
array[k]=aux[j++];
}else if (j>high)
{
array[k] = aux[i++];
} else if (aux[j] < aux[i]) {
array[k] = aux[j++];
} else {
array[k] = aux[i++];
}
}
}
private static void sort(int[] array , int low ,int high){
if (low>=high) return;
int mid = low+(high-low)/2;
sort(array,low,mid);
sort(array, mid+1, high);
merge(array,low,mid,high);
}
private static void 归并排序(int[] array){
aux = new int[array.length];
sort(array,0,array.length-1);
}
基数排序
基本思想
基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine), 排序器每次只能看到一个列。它是基于元素值的每个位上的字符来排序的。 对于数字而言就是分别基于个位,十位, 百位或千位等等数字来排序。
基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序按照优先从高位或低位来排序有两种实现方案:
- MSD(Most significant digital) 从最左侧高位开始进行排序。先按k1排序分组, 同一组中记录, 关键码k1相等, 再对各组按k2排序分成子组, 之后, 对后面的关键码继续这样的排序分组, 直到按最次位关键码kd对各子组排序后. 再将各组连接起来, 便得到一个有序序列。MSD方式适用于位数多的序列。
- LSD (Least significant digital)从最右侧低位开始进行排序。先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。LSD方式适用于位数少的序列。
算法描述
我们以LSD为例,从最低位开始,具体算法描述如下:
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|
O(d*(n+r)) | O(d*(n+r)) | O(d*(n+r)) | O(n+r) |
其中,d 为位数,r 为基数,n 为原数组个数。在基数排序中,因为没有比较操作,所以在复杂上,最好的情况与最坏的情况在时间上是一致的,均为 O(d*(n + r)) 。
Java实现
private static void 基数排序(int[] array){
if (array.length<=1){
return;
}
int max = 0;
for (int i = 0; i < array.length; i++) {
if (max < array[i]) {
max = array[i];
}
}
int maxDigit = 1;
while(max/10>0){
maxDigit++;
max = max / 10;
}
int[][] buckets = new int[10][array.length];
int base = 10;
for (int i = 0; i < maxDigit; i++) {
int[] bktLen = new int[10];
for (int j = 0; j < array.length; j++) {
int whichBucket = (array[j] % base) / (base / 10);
buckets[whichBucket][bktLen[whichBucket]] = array[j];
bktLen[whichBucket]++;
}
int k = 0;
for (int b = 0; b < buckets.length; b++) {
for (int p = 0; p < bktLen[b]; p++) {
array[k++] = buckets[b][p];
}
}
base *= 10;
}
}
堆排序
堆的定义如下:n
个元素的序列{k1,k2,…,kn} 当且仅当满足下关系时,称之为堆。
把此序列对应的二维数组看成一个完全二叉树。那么堆的含义就是:完全二叉树中任何一个非叶子节点的值均不大于(或不小于)其左,右孩子节点的值。 由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。因此我们可使用大顶堆进行升序排序, 使用小顶堆进行降序排序。
基本思想
此处以大顶堆为例,堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序。
由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列。 同时由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序。
算法描述
https://www.bilibili.com/video/BV1B64y1975b
- 建立堆的过程, 从length/2 一直处理到0, 时间复杂度为O(n);
- 调整堆的过程是沿着堆的父子节点进行调整, 执行次数为堆的深度, 时间复杂度为O(lgn);
- 堆排序的过程由n次第2步完成, 时间复杂度为O(nlgn).
Java实现
关于树的公式复习:
- 非空二叉树叶子结点数 = 度为2的结点数 + 1 即,N0=N2+1
- 非空二叉树上第K层至多有2k?1 个结点(K≥1)
- 高度为H的二叉树至多有2H?1 个结点(H≥1)
- 具有N个(N>0)结点的完全二叉树的高度为 ?log2(N+1)? 或 ?log2N?+1
- 对完全二叉树按从上到下、从左到右的顺序依次编号1,2,…,N,则有以下关系:
- 当 i>1 时,结点 i 的双亲结点编号为 ?i/2? ,即当 i 为偶数时,其双亲结点的编号为 i/2 ,它是双亲结点的左孩子;当 i 为奇数时,其双亲结点的编号为 (i?1)/2 ,它是双亲结点的右孩子。
- 当 2i≤N 时,结点i的左孩子编号为 2i ,否则无左孩子。
- 当 2i+1≤N 时,结点i的右孩子编号为 2i+1 ,否则无右孩子。
- 结点 i 所在层次(深度)为 ?log2i?+1 。(设根结点为第1层)
private static void 堆排序(int[] a){
for (int i = a.length - 1; i > 0; i--) {
max_heapify(a, i);
int temp = a[0];
a[0] = a[i];
a[i] = temp;
}
}
public static void max_heapify(int[] a, int n) {
int child;
for (int i = (n - 1) / 2; i >= 0; i--) {
child = 2 * i + 1;
if (child != n && a[child] < a[child + 1]) {
child++;
}
if (a[i] < a[child]) {
int temp = a[i];
a[i] = a[child];
a[child] = temp;
}
}
}
做法二:
private static void 堆排序_v2(int[] array){
for (int i = array.length/2-1 ;i>=0 ; i--){
adjustHeap(array,i,array.length);
}
for (int j = array.length-1;j>0 ; j--){
int temp = array[j];
array[j] = array[0];
array[0] = temp;
adjustHeap(array,0,j);
}
}
public static void adjustHeap(int[] array,int i,int length){
int temp = array[i];
for (int k = i*2+1; k<length; k=k*2+1){
if (array[k]<array[k+1]&&(k+1)<length){
k++;
}
if (array[k]>temp){
array[i] = array[k];
i = k;
}else {
break;
}
}
array[i] = temp;
}
补充:二分查找
基本思想与算法描述
二分查找(binary search),也称折半搜索,是一种在 有序数组 中 查找某一特定元素 的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
- 时间复杂度:折半搜索每次把搜索区域减少一半,时间复杂度为O(log n)。(n代表集合中元素的个数)
- 空间复杂度: O(1)。虽以递归形式定义,但是尾递归,可改写为循环。
**如何计算二分查找中的中值?**大家一般给出了两种计算方法:
- 算法一:
mid = (low + high) / 2 - 算法二:
mid = low + (high – low)/2
乍看起来,算法一简洁,算法二提取之后,跟算法一没有什么区别。但是实际上,区别是存在的。**算法一的做法,在极端情况下,(low + high)存在着溢出的风险,进而得到错误的mid结果,导致程序错误。**而算法二能够保证计算出来的mid,一定大于low,小于high,不存在溢出的问题。
二分查找法的O(log n)让它成为十分高效的算法。不过它的缺陷却也是那么明显的。就在它的限定之上:必须有序,我们很难保证我们的数组都是有序的。当然可以在构建数组的时候进行排序,可是又落到了第二个瓶颈上:它必须是数组。
数组读取效率是O(1),可是它的插入和删除某个元素的效率却是O(n)。因而导致构建有序数组变成低效的事情。
解决这些缺陷问题更好的方法应该是使用二叉查找树了,最好自然是自平衡二叉查找树了,既能高效的(O(n log n))构建有序元素集合,又能如同二分查找法一样快速(O(log n))的搜寻目标数。
Java实现
private static int binary_search_a(int[] array,int left_limit,int right_limit,int target){
if (left_limit>right_limit){
return -1;
}
int mid = left_limit+(right_limit-right_limit)/2;
if (array[mid]>target){
return binary_search_a(array,left_limit,mid-1,target);
}
if (array[mid]<target){
return binary_search_a(array,mid+1,right_limit,target);
}
System.out.println("有查找到:"+array[mid]+" position: "+(mid+1));
return mid+1;
}
private static void binary_search_b(int[] array,int target){
int left_limit = 0,right_limit = array.length-1;
while (left_limit<right_limit){
int mid = left_limit+(right_limit-right_limit)/2;
if (array[mid]>target){
right_limit=mid-1;
}else if (array[mid]<target){
left_limit=mid+1;
}else {
System.out.println("有查找到:"+array[mid]+" position: "+(mid+1));
return;
}
}
System.out.println("未查找到: "+target+"!");
return;
}
|