首先引入一个例子:假如班级考试成绩排名,首先根据总成绩确定名次。那如果总成绩有相同的情况,那么再按照数语外三科成绩之和进行排名,如此以来我们需要比对两次。那么我们可以考虑,把这两个关键字(总成绩、数语外三科成绩)的值拼接在一起,位数不够的补零,这样以来就能直接比较出个优劣来。
比如:两个同学的总成绩都是456,但是数语外三科成绩和分别为230和198,拼接起来就是456230和456198,然后就可以直接进行比较。
也就是说:个关键字的排序最终都可以转化为单个关键字的排序,这也算是一个小技巧。
然而这里的主题并不是举类似于学生排名的例子。而是为了引出排序的概念。
排序前我们先来认识一下内排序和外排序:根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序。内排序是在排序整个过程中,待排序的所有记录全部被就置在内存中 。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
对于内排序,影响其性能的主要因素有以下几项:
1. 时间性能
排序是数据处理中经常执行的一种操作,往往属于系统的核心部分,因此排序算法的时间开销是衡量其好坏的最重要的标志。在内排序中,主要进行两种操作:比较和移动 。 比较指关键字之间的比较,这是要做排序最起码的操作。 移动指记录从一个位置移动到另一个位置,事实上,移动可以通过改变记录的存储方式来予以避免(这个我们在讲解具体的算法时再谈)。总之,高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。
2. 辅助空间
评价排序算法的另一个主要标准是执行算法所需要的辅助存储空间。辅助存储空间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间 。
3 . 算法的复杂性
注意这里指的是算法本身的复杂度,而不是指算法的时间复杂度。显然算法过于复杂也会影响排序的性能。(摘自《大话数据结构》一书)
内排序可分为:插入排序、交换排序、选择排序、归并排序。
排序算法本系列介绍七种:冒泡排序、简单选择排序、直接插入排序、希尔排序、堆排序、归并排序、快速排序。前三者属于简单排序算法,后四者属于优化排序算法。
本系列来介绍三种冒泡排序的java实现方法:
公用代码,下边三种实现方法均能用到:
/**
* 交换数组下标i和j的值
*
* @param arr 用于存储排序数组
* @param i 下标i
* @param j 下标j
*/
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
一、最简单的实现
public static void sort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
SortUtil.swap(arr, i, j);
}
}
}
}
public static void main(String[] args) {
int[] arr = {17, 23, 13, 21, 9};
sort(arr);
for (int tem : arr) {
System.err.print(tem + " ");
}
}
比较流程:
- i=0,j=1时,17会和23先比较,不做交换;接着和13比较,交换;接着13和21比较,不做交换;接着和9比较,交换;注意此时把比较小的数13也“顺带”给挪到最后了,这并不能对后续继续排序起便捷作用
- i=1,j=2时,17和23比较,不做交换;接着和21比较,不做交换;继续和13比较,交换;注意,到这里17和23比较了两次,在性能上讲的话,并不合理
- i=2,j=3时,23和21比较,交换;接着21和17比较,交换;注意,在第二次循环时17就和21比较过一次,由于17小于21,并没有发生交换,说明二者顺序是排好的,到这次循环又发现21被挪到了17的前边,故又进行了一次交换,无用功。
- i=3,j=4时,23和21比较,交换;好了,这里又出现了第三次循环发生的问题。
public static void sort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = arr.length - 1; j > i; j--) {
if (arr[j - 1] > arr[j]) {
SortUtil.swap(arr, j - 1, j);
}
}
}
}
public static void main(String[] args) {
int[] arr = {17, 23, 13, 21, 9};
sort(arr);
for (int tem : arr) {
System.err.print(tem + " ");
}
}
排序流程:
三、冒泡排序继续优化
到这就算是结束了么?我们尽可能考虑一些几段情况,比如有数组int[] arr = {2, 1, 3, 4, 5, 6, 7, 8, 9};现在需要用上述第二种优化后的排序方式进行排序,我们会有怎样的疑虑呢?很显然,在第一次(也就是i=0)循环时把2和1进行了位置调换后,整个数组已经处于有序状态,可是程序不会感知到这种奇怪的地方,它依然不依不挠地按部就班死乞白赖地往后执行剩余的8个,准确说是7个循环,而这些虽然不会进行数据交换,但还是发生了循环,所以呢,基于方法总是比问题多的原理,请看下边的进一步优化方案:
public static void sort(int[] arr) {
boolean bool = true;
for (int i = 0; i < arr.length && bool; i++) {
bool = false;
for (int j = arr.length - 1; j > i; j--) {
if (arr[j - 1] > arr[j]) {
SortUtil.swap(arr, j - 1, j);
bool = true; // 说明这次循环进行了数据交换,不保证下次就没有数据交换操作发生
}
}
}
}
public static void main(String[] args) {
int[] arr = {2, 1, 3, 4, 5, 6, 7, 8, 9};
sort(arr);
for (int tem : arr) {
System.err.print(tem + " ");
}
}
如此以来,性能就又有所提升,可以避免因已经有序的情况下的无意义循环判断。
四、时间复杂度分析
到这里,冒泡排序的优化方案也就“差不多行了”,下边来看一下其时间复杂度大O表示法,从上边的分析可以发现第二种的算法时间复杂度为:
(n - 1)+ (n - 2) + ... + 2 + 1 = n * (n - 1)/ 2 所以时间复杂度为O(n2)。
第三种方案,最理想的情况下算法复杂度为只进行了一次for循环,时间复杂度为O(n),但是不得不考虑所有情况,所以其时间复杂度同样为O(n2)。
注:上边代码和分析均参考了《大话数据结构》一书,作者程杰。感兴趣的可以读一下。