常见排序算法总结(一)

常见排序算法总结(一)

排序就是将一组对象按照某种逻辑顺序重新排列的过程

本篇文章的程序代码基本结构如下:

import java.util.Scanner;

public class Example {
	public static void sort(int[] a) {
		//TODO
        //编写排序算法
	}
	
	private static boolean less(int v, int w) {
		return v < w;
	}
	
	private static void exch(int[] a, int i, int j) {
		int t = a[i];
		a[i] = a[j];
		a[j] = t;
	}
	
	private static boolean isSorted(int[] a) {
        //测试数组元素是否有序
		for (int i = 1; i < a.length; i++) {
			if (less(a[i], a[i-1])) {
				return false;
			}
		}
		return true;
	}
	
	private static void show(int[] a) {
		for (int i = 0; i < a.length; i++) {
			System.out.print(a[i] + " ");
		}
		System.out.println();
	}
	
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		int n = in.nextInt();
		int[] a = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = in.nextInt();
		}
		in.close();
		
		sort(a);
		assert isSorted(a);
		show(a);
	}
	
}

选择排序

一种最简单的排序算法是这样的:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组中的第二个元素交换位置。如此往复,直到将整个数组排序。这种方法叫做选择排序,因为它在不断地选择剩余元素中的最小值。

代码如下所示:

public static void sort(int[] a) {
    //将a[]按升序排序
    int N = a.length;
    for(int i = 0; i < N; i++) {
        //将a[i]和a[i+1...N]中最小的元素交换
        int min = i; //最小元素的索引
        for(int j = i + 1; j < N; j++) {
            if(less(a[j], a[min])) {
                min = j;
            }
        }
        exch(a, i, min);
    }
}

该算法将第i小的元素放到a[i]之中。数组的第i个位置的左边是i个最小的元素且他们不会再被访问。

选择排序的内循环只是在比较当前元素与已知的最小元素(以及将当前索引加1和检查是否代码越界),这已经简单到了极点。交换元素的代码写在内循环之外,每次交换都能排定一个元素,因此交换的总次数为N。所以算法的时间效率取决于比较的次数。

总的来说,选择排序是一种很容易理解和实现的简单排序算法,它有两个很鲜明的特点:

  • 运行时间和输入无关。为了找出最小的元素而扫描一边数组并不能为下一次扫描提供什么信息。这种性质在某种情况下是缺点,因为使用选择排序的人可能会惊讶的发现,一个已经有序的数组或是逐渐全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!我们将会看到,其他算法会更善于利用输入的初始状态。
  • 数据移动是最少的。每次交换都会改变两个数组元素的值,因此选择排序用了N次交换交换次数和数组的大小是线性关系

插入排序

通常人们打牌时整理自己的手牌时,都是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。这种算法叫做插入排序。

public static void sort(int[] a) {
    int N = a.length;
    for (int i = 1; i < N; i++) {
        // 将a[i]插入到a[i-1],a[i-2],a[i-3]...之中
        for (int j = i; j > 0 && less(a[j], a[j-1]); j--) {
            exch(a, j, j-1);
        }
    }
}

对于1到N-1之间的每一个i,将a[i]和a[0]到a[i-1]中比它小的所有元素依次有序地交换。在索引i由左向右变化的过程中,它左侧的元素总是有序的,所以当i到达数组的右端时排序就完成了。

与选择排序一样,当前索引左边的所有元素都是有序的,但他们的最终位置还不确定,为了给更小的元素腾出空间,他们可能会被移动。但是当索引到达数组的右端时,数组排序就完成了。

和选择排序不同的是,插入排序所需的时间取决与输入中元素的初始位置。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。

插入排序对于实际应用中常见的某些类型的非随机数组很有效。插入排序能够立即发现每个元素都已经在合适的位置之上,它的运行时间也是线性的(对于这种数组,选择排序的运行时间是平方级别的)。对于所有主键都相同的数组也会出现相同的情况

希尔排序

一种基于插入排序的快速的排序算法

对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点得从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1次移动。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

希尔排序的思想是使数组中任意间隔为h的元素都是有序的。这样的数组成为h有序数组。换句话说,一个h有序数组就是h个互相独立的有序数组编织在一起组成的一个数组。在进行排序时,如果h很大,我们就能将元素移动到很远的地方,为实现更小的h有序创造方便。用这种方式,对于任意以1结尾的h序列,我们都能将数组排序。这就是希尔排序。

public static void sort(int[] a) {
    int N = a.length;
    int h = 1;
    while (h < N/3) {
        h = 3 * h + 1;
    }
    while (h >= 1) {
        //将数组变为h有序
        for (int i = h; i < N; i++) {
            //将a[i]插入到a[i-h], a[i-2*h], a[i-3*h]..之中
            for (int j = i; j >= h && less(a[j], a[j-h]); j-=h) {
                exch(a, j, j-h);
            }
        }
        h = h/3;
    }
}

如果我们在插入排序中加入一个外循环来将h按照递增序列递减,我们就能得到这个简洁的希尔排序。增幅h的初始值是数组长度乘以一个常数因子,最小为1。

实现希尔排序的一种方法是对于每个h,用插入排序将h个子数组独立的排序。但因为子数组是相互独立的,一个更简单的方法是在h-子数组中将每个元素交换到比它大的元素之前去(将比它大的元素向右移动一格)。只需要在插入排序的代码中将移动元素的距离由1改为h即可。这样,希尔排序的实现就转化为了一格类似于插入排序但是用不同增量的过程。

希尔排序最高效的原因在于它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。子数组部分有序的程度取决于递增序列的选择。

归并排序

归并,即将两个有序的数组归并成一个更大的有序数组。要将一个数组排序,可以先(递归地)将它分成两半进行排序,然后将结果归并起来。归并排序最吸引人的性质是它能够保证将任意长度为N的数组排序所需时间和NlogN成正比;它的主要缺点则是它所需的额外空间和N成正比。

原地归并的抽象方法

实现归并的一个直截了当的方法就是将两个不同的有序数组归并到第三个数组中。实现的方法很简单,创建一个适当大小的数组然后将两个输入数组中的元素一个个从小到大放入这个数组中。

但是,当归并将一个大数组排序时,我们需要进行很多次归并,因此在每次归并时都创建一个新数组来存储排序结果会带来问题。我们更希望有一种能够原地归并的方法,这样就可以现将前半部分排序,再将后半部分排序,然后在数组中移动元素而不是使用额外的空间。

下面的代码实现了这种归并。它将涉及到的所有元素复制到一个辅助数组中,再将归并的结果放回到原数组。

public static void merge(int[] a, int low, int mid, int high) {
    int i = low, j = mid + 1;
    for (int k = low; k <= high; k++) {
        aux[k] = a[k];
    }
    for (int k = low; k <= high; k++) {
        if (i > mid) {
            a[k] = aux[j++];
        } else if (j > high) {
            a[k] = aux[i++];
        } else if (less(aux[j], aux[i])) {
            a[k] = aux[j++];
        } else {
            a[k] = aux[i++];
        }
    }
}

该方法现将所有元素复制到aux[]中,然后再归并会a[]中。方法在归并时进行了4个条件判断:左半边用尽(取右半边的元素),右半边用尽(取左半边元素),右半边的当前元素小于左半边的当前元素(取右半边的元素)以及右半边的当前元素大于等于左半边的当前元素(取左半边的元素)。

自上而下的归并排序:

public class Merge {
	private static int[] aux; //归并所需的辅助数组
	
	public static void sort(int[] a) {
		aux = new int[a.length];
		sort(a, 0, a.length - 1);
	}
	
	private static void sort(int[] a, int low, int high) {
		if (high <= low) {
			return;
		}
		int mid = (low + high) / 2;
		sort(a, low, mid); //将左半边排序
		sort(a, mid, high); //将右半边排序
		merge(a, low, mid, high); //归并结果merge方法见上面实现
	}
}

本算法基于原地归并的抽象实现了另一种递归归并,这也是应用高效算法设计中分治思想的最典型的一个 例子。这段递归代码是归纳证明算法能够正确地将数组排序的基础:如果它能够将两个子数组排序,他就能通过归并两个子数组来将整个数组排序。

要对子数组a[low..high]进行排序,先将它分为a[low..mid]和a[mid+1..high]两部分,分别通过递归调用将它们单独排序,最后将有序的子数组归并为最终的排序结果。

自底向上的归并排序

实现归并排序的另一种方法是先归并那些微型数组,然后再成对归并得到的子数组,如此这般,直到我们将整个数组归并到一起。这种实现方法比标准递归方法所需要的代码量更少。首先我们进行的是两两归并(把每个元素想象成一个大小为1的数组),然后是四四归并(将两个大小为2的数组归并成一个有4个元素的数组),然后是八八归并,一直下去。在每一轮归并中,最后一次归并的第二个子数组可能比第一个子数组小(这对merge()方法不是问题),如果不是的话所有的归并中两个数组大小都应该一样,而在下一轮中子数组的大小会翻倍。

自底而上的归并排序算法的实现如下:

public static void sort(int[] a) {
    int N = a.length;
    aux = new int[N];
    //sz 子数组大小
    //lo 子数组索引
    for (int sz = 1; sz < N; sz = sz + sz) {
        for (int low = 0; low < N - sz; low += sz + sz) {
            merge(a, low, low + sz - 1, Math.min(low + sz + sz - 1, N - 1));
        }
    }
}

自底向上的归并排序会多次遍历整个数组,根据子数组大小进行两两归并。子数组的大小sz的初始值为1,每次加倍。最后一个子数组的大小只有在数组大小是sz的偶数倍时候才会等于sz(否则它会比sz小)。

原文地址:https://www.cnblogs.com/hh09cnblogs/p/13752135.html