目录
前言
排序分为内排序和外排序,内排序是指排序时不涉及数据的内、外存交换。本章讨论的是内排序。内排序主要有8种方式,它们各有优劣。
一、排序的基本概念
1.排序的定义
所谓排序,就是整理表中的元素,使之按照关键字递增或递减有序排列。在默认情况下所有的排序均指的是递增排序。
2.内排序的分类
根据内排序算法是否基于关键字的比较,将内排序算法分为基于比较的排序算法和不基于比较的排序算法。插入排序、交换排序、选择排序和归并排序都是基于比较的排序算法,而基数排序则是不基于比较的排序算法。
3.排序的稳定性
当待排序的表中存在多个关键字相同的元素,经过排序后这些具有相同关键字的元素之间的相对次序保持不变,则称这种排序方法是稳定的。反之,如果这些元素的相对次序改变了,则称这种排序方法是不稳定的。
4.有序区与无序区
在排序过程中的某一时刻R被划分为两个区间,前面的子区间是已经排好序的,称作有序区,后面的是待排序的部分,称为无序区
5.排序数据的组织
在讨论排序算法时,以顺序表作为排序数据的存储结构,假设关键字为int类型,其元素类型定义如下:
public class SortType {
int key; //存放关键字
String data; //存放其他数据
public SortType(int key){
this.key = key;
}
}
设计用于内排序的SortClass如下:
public class SortClass {
final int MAXSIZE = 100; //最多元素的个数
SortType[] R; //存放排序的元素
int num; //表中实际元素的个数
/**
* 交换i和j
* @param i
* @param j
*/
public void swap(int i,int j){
SortType tmp;
tmp = R[i];
R[i] = R[j];
R[j] = tmp;
}
/**
* 由关键字序列a构造顺序表R
* @param a
*/
public void creatR(int[]a){
R = new SortType[MAXSIZE];
for (int i = 0; i < a.length; i++) {
R[i] = new SortType(a[i]);
}
num = a.length;
}
/**
* 构造用于堆排序的R
* @param a
*/
public void creatR_1(int[]a){
R = new SortType[MAXSIZE];
for (int i = 0; i < a.length; i++) {
R[i+1] = new SortType(a[i]);
}
num = a.length;
}
/**
* 输出顺序表R
*/
public void display(){
for (int i = 0; i < num; i++) {
System.out.print(R[i].key+" ");
}
System.out.println();
}
/**
* 输出用于堆排序的R
*/
public void display_1(){
for (int i = 1; i <= num; i++) {
System.out.print(R[i].key+" ");
}
System.out.println();
}
}
二、插入排序
插入排序的基本思想是每次将一个待排序的元素按其关键字大小插入前面已经排好序的子表中的适当位置
1.直接插入排序
直接插入排序的每趟操作是将当前无序区的开头元素插入有序区中适当位置,从而扩大有序区减少无序区。这种方法通常称为增量法,因为它每次使有序区增加一个元素。经过n-1趟后无序区变空。
其过程为:先将无序区头元素R[i]暂时放入tmp中,用j在有序区从后往前找,凡是大于tmp的元素均后移一个位置,指到找到某个小于或等于tmp的元素为止,再将tmp放在它的后面
public void insertSort(){
SortType tmp;
int j;
for (int i = 1; i < num; i++) { //从R[1]开始
if (R[i].key < R[i - 1].key) { //当反序时才执行,可以减少运行次数
tmp = R[i]; //取出无序区第一个元素
j = i - 1; //有序区的末尾
while (j >= 0 && tmp.key < R[j].key) {
R[j + 1] = R[j]; //将所有比tmp大的元素后移
j--;
}
R[j + 1] = tmp; //在j+1处插入tmp
}
}
}
2.折半插入排序
折半查找是先在有序区中用折半查找方法找到插入位置,再通过移动元素经行插入。折半插入排序又称为二分插入排序
public void halfSort(){
SortType tmp;
int left;
int right;
int mid;
for (int i = 1; i < num; i++) {
if (R[i].key < R[i - 1].key) { //当反序时才执行,可以减少运行次数
tmp = R[i]; //取出无序区第一个元素
left = 0;
right = i - 1;
while (left <= right) { //折半查找插入位置
mid = (left + right) / 2; //取中间位置
if (R[mid].key <= tmp.key) {
left = mid + 1; //插入点在左边
} else {
right = mid - 1; //插入点在右边
}
}
for (int j = i - 1; j >= right + 1; j--) { //元素集中后移
R[j + 1] = R[j];
}
R[right + 1] = tmp; //插入tmp
}
}
}
3.希尔排序
希尔排序是一种采用分组插入排序的方法。其基本思想是先取一个小于n的整数d作为第一个增量,将全部元素分为d组,所有相聚d的元素为一组,再对各组进行插入排序。然后取第二个小于d的增量,重复进行分组和排序。
我们可以看到,两个2的相对位置发生了改变,所以希尔排序是一种不稳定的排序方法。其代码如下:
public void shellSort(){
SortType tmp;
int d = num/2; //增量初始值
while (d>0){
for (int i = d; i < num; i++) { //对所有相隔d的单位进行直接插入排序
tmp = R[i];
int j = i-d;
while(j>=0 && tmp.key<R[j].key){
R[j+d] = R[j]; //对相隔d的单位排序
j=j-d;
}
R[j+d] = tmp;
}
d=d/2; //递减增量
}
}
三、交换排序
交换排序的思想是两两比较待排序元素的关键字,当这两个元素反序时交换,直到没有反序的元素为止。
1.冒泡排序
冒泡排序也成为气泡排序,是一种典型的交换排序方法,其基本思想是将无序区中的相邻元素之间的比较和交换使最小(或最大)的元素如气泡一样逐渐靠近最前端。
在冒泡排序中,如果某一趟没有出现任何元素交换,说明所有元素已经排好序了,可以结束算法。
/**
* 冒泡排序;双循环
*/
public void bubbleSort(){
boolean flag = false;
for (int i = 0; i < num; i++) {
flag = false; //每趟前将flag置为false
for (int j = num-1; j > i; j--) { //在一趟中找到最小关键字的元素
if(R[j-1].key>R[j].key){ //反序使交换
swap(j,j-1);
flag = true; //本趟发生了交换,置flag为true
}
}
if(!flag){
return; //如果未发生交换,则结束算法
}
}
}
2.快速排序
快速排序是由冒泡排序改进得出的,它的基本思路是取待排序表的第一个元素作为基准,将基准归位,并将所有小于基准的元素放到基准的前面(构成左子表),所有大于基准的放到基准的后面。然后对左右子表重复上述过程。
快速排序的核心是划分算法,即如何将基准归位并完成左、右子表的构建。代码如下:
private int partition(int left,int right){
SortType tmp;
tmp = R[left]; //取出无序区首元素作为基准
int i = left;
int j = right;
while (left<right) {
while (right > left) { //在右边寻找比基准小的元素
if (R[right].key < tmp.key) {
R[left] = R[right];
left++;
break;
}
right--;
}
while (left < right) { //在左边寻找比基准大的元素
if (R[left].key > tmp.key) {
R[right] = R[left];
right--;
break;
}
left++;
}
}
R[left] = tmp; //将基准归位
return left; //返回归位的位置
}
对于int[] arr = {3,7,2,5,1},其划分过程如下:
:
剩下的代码如下:
public void quickSort(){
quickSortR(0,num-1);
}
private void quickSortR(int left,int right){
if(left>=right){ //递归出口,如果left大于等于right表示该区间内所有元素都已归位
return;
}
int i = partition(left,right); //划分算法
quickSortR(left,i); //递归排序左子表
quickSortR(i+1,right); //递归排序右子表
}
四、选择排序
选择排序的基本思想是将排序序列分为有序区和无序区,每一趟排序从无序区中选出最小的元素放在有序区的最后,从而扩大有序区。
1.简单选择排序
要从无序区中选出最小元素,最简单的方法是逐个元素进行比较。
public void selectSort(){
for (int i = 0; i < num - 1; i++) {
int min = i;
for (int j = i+1; j < num; j++) { //在无序区中选最小元素min
if(R[j].key<R[min].key){
min = j;
}
}
if(min!=i){ //如果min不是无序区的首元素
swap(i,min); //将min置于无序区首元素
}
}
}
2.堆排序
堆排序是简单选择排序的改进,利用二叉树代替选择方法来找最大或最小元素,属于一种树形选择排序方法。我们将排列序列看作一颗完全二叉树的顺序存储结构,利用其双亲结点和孩子结点之间的内在关系,将其调整为堆。
1)堆的定义
堆是n个关键字序列k1、k2、...、kn,且满足以下性质(称为堆性质):(1)ki<k2i且ki<k2i+1 或 (2)ki>k2i且ki>k2i+1。满足第一种情况的称为小根堆,满足第二种情况的称为大根堆
2)筛选算法
堆排序的核心是筛选算法,用于将完全二叉树调整为大根堆。其过程是先将i指向根结点R[low],用tmp保存根结点,j指向它的左孩子(j=2i),在j<=high时循环:
(1)若R[i]的右孩子比较大,则让j指向其右孩子。
(2)若最大孩子R[j]比双亲R[i]大,则swap(i,j)。这样做可能会破坏堆性质,所以继续筛选R[j]的子树。
(3)若最大孩子R[j]比双亲R[i]小,说明已经满足堆性质,退出循环。
对应的筛选算法如下:
public void sift(int low,int high){
int i = low;
int j = 2*i; //R[j]是R[i]的左孩子
while (j<=high){ //只对R[low...high]之间的元素进行筛选
if(j<high && R[j].key<R[j+1].key){
j++; //若右孩子较大,则j指向右孩子
}
if(R[i].key<R[j].key){ //若R[i]的孩子结点较大
swap(i,j); //R[i]和R[j]交换
i = j;
j = 2*i;
}else {
break; //若双亲结点较大,则结束
}
}
}
3)建立初始堆
对于一颗完全二叉树,编号为n-2的结点是最后一个分支结点,按从i=n/2到1的顺序调用筛选算法。
for (int i = num/2; i > 0 ; i--) {
sift(i,num);
}
4)堆排序算法
在初始堆构建好后,根结点一定是最大关键字结点,将其放到排序序列的最后。由于最大元素的归位,待排序元素减少一个,但由于根结点的改变,前面n-1个结点不一定为堆,而其左、右子树均为堆,调用一次筛选方法使其成堆。重复以上操作,知道完全二叉树中只剩一个结点为止。算法如下:
public void heapSort(){
for (int i = num/2; i > 0 ; i--) {
sift(i,num);
}
for (int i = num; i > 0; i--) { //进行num次排序,每次排序中的元素个数减一
swap(1,i); //将该区间中最后一个元素与R[1]交换
sift(1,i-1); //从R[1]继续筛选,得到i-1个结点的堆
}
}
五、归并排序
归并排序的原理是多次将两个或以上的相邻有序表合并成一个新的有序表。根据归并的路数,归并排序分为二路、三路和多路排序。这里主要讨论二路排序,二路归并排序又分为自底向上和自顶向下两种方法。
1.自底向上的二路归并排序
1)排序思路
二路归并是将两个有序子表合并成一个有序表,二路归并排序是利用二路归并实现的,其思路是将待排序序列看成num个长度为1的有序子表,然后再进行两两相邻的有序子表的归并,得到num/2个长度为2的有序子表,再两两合并。。。以此类推,直到得到一个长度为n的有序表为止。
2)二路归并算法
我在数据结构笔记(单链表)中的合并链表问题有介绍过二路归并算法,这里采用相同的思路。对应的算法如下:
private void Merge(int low,int mid,int high){ //将R[low..mid]和R[mid+1...high]归并为R[low...high]
SortType[] R1 = new SortType[high-low+1];
int i = low; //i作为第一段的下标
int j = mid+1; //j作为第二段的下标
int k = 0; //k为R1的下标
while (i<=mid && j<=high){ //将第一段中的元素放入R1中
if(R[i].key<=R[j].key){
R1[k] = R[i];
i++;
k++;
}else { //将第二段的元素放入R1中
R1[k] = R[j];
j++;
k++;
}
}
while (i<=mid){ //将第一段剩下的元素放入R1中
R1[k] = R[i];
i++;
k++;
}
while (j<=high){
R1[k] = R[j];
j++;
k++;
}
for(k=0,i=low;i<=high;k++,i++){ //将第二段剩下的元素放入R1中
R[i] = R1[k];
}
}
3)每一趟的二路归并排序
在某趟归并中,设有序子表的长度为len,则归并前R可分为num/len个有序子表。
private void MergePass(int len){
int i;
for(i=0;i+2*len-1<num;i=i+2*len){ //归并len长的两个相邻子表
Merge(i,i+len-1,i+2*len-1);
}
if(i+len<num){ //如果余下的子表后者长度小于len
Merge(i,i+len-1,num-1); //归并这两个子表
}
}
4)二路归并排序算法
在二路归并排序中,len从1开始调用MergePass,之后每趟的len倍增,直到len大于等于num为止,就可以得到长度为num的有序表。
public void MergeSort1(){
for(int len = 0;len < num;len = 2*len){
MergePass(len);
}
}
2.自顶向下的二路归并排序
思路是先将长度为num的排序序列分解为n个长度为1的有序段,再调用Merge方法进行合并,由于采用递归实现,又称为递归二路合并。
public void MergeSort2(){
MergeSort2R(0,num-1);
}
private void MergeSort2R(int s,int t){
if(s>=t){
return; //当待拆分序列为0或1时返回
}
int m = (s+t)/2; //取中间位置m
MergeSort2R(s,m); //对前子表排序
MergeSort2R(m+1,t); //对后子表排序
Merge(s,m,t); //将两个有序子表合并
}
总结
没有哪一种排序方法是绝对好的,每一种排序方法都有其优缺点,适合于不同的场景,因此在实际应用中要根据情况做选择。如果要求稳定性,则只能在稳定方法中选择。