在java排序算法中,按时间复杂度分类的话可以分为三类。如下:
(1)O(n*n)的排序算法,有插入排序、冒泡排序、选择排序;
(2)O(n*lgn)的排序算法,有归并排序、堆排序以及快速排序;
(3)O(n)的排序算法,有基数排序、计数排序及桶排序。
O(n*n)的排序算法
(1)插入排序
算法思路:首先以第一个数为基数,取出第二个数和基数做对比,如果大于基数则放在基数的右边,反之则放在基数的左边。依次取出后面的数(j)则依次和0..j-1中的每个数做对比,找到某个位置使得此数大于左边的数小于等于右边的数。
代码如下:
private void insertSort(int[] origin){
if(origin != null && origin.length > 1){
for(int i=1; i<origin.length - 1; i++){
int cur = origin[i];
int j = i - 1;
while(j >= 0 && cur < origin[j]){
int temp = origin[j];
origin[j] = cur;
origin[j + 1] = temp;
j--;
}
}
}
}
(2)冒泡排序
算法思路:所有的数据从左至右,两两比较,如果左数比右数大,则交换位置,反之则位置不变,每扫描一轮则会找到一个最大值排在最后面,重复n-1轮就会排序完毕。
代码如下:
private void bubbleSort(int[] origin){
if(origin != null && origin.length > 1){
boolean isChanged = false;
for (int i = 1; i <= origin.length; i++) {
isChanged = false;
for (int j = 0; j < origin.length - i; j++) {
if (origin[j] > origin[j + 1]) {
int tmp = origin[j];
origin[j] = origin[j + 1];
origin[j + 1] = tmp;
isChanged = true;
}
}
if (!isChanged) {
break;
}
}
}
}
(3)选择排序
排序思路:扫描所有数据,找到最大的和最右侧的数据交换。或找到最小的和最左侧的数据交换。
代码如下:
public void selectSort(int[] origin){
if(origin != null && origin.length > 1){
for(int i=0; i<origin.length; i++){
int maxIndex = 0;
for(int j=1; j<origin.length - i; j++){
if(origin[j] > origin[maxIndex]){
maxIndex = j;
}
}
int lastIndex = origin.length - i - 1;
if(maxIndex != lastIndex){
int tmp = origin[lastIndex];
origin[lastIndex] = origin[maxIndex];
origin[maxIndex] = tmp;
}
}
}
}
O(n*lgn)的排序算法
(1)归并排序(merge sort)
归并排序采用的策略叫做“分治策略”,分治策略有下面三个步骤:
1. 分解原问题为若干子问题;
2. 解决这些子问题,子问题较大就继续分解子问题,递归求解各个子问题,当子问题足够小,通常就可以直接解决;
3. 合并这些子问题的解,合并成为原问题的解。
归并排序:分解待排序的n个元素序列为各有n/2个元素的两个子序列,然后递归的分解左右两个子序列,最后到无法再分解为止,递归合并这些子序列以产生排序的最后结果。
代码如下:
private void mergeSort(int[] origin){
if(origin != null && origin.length > 1){
mergeSortInternal(origin, 0, origin.length - 1);
}
}
private void mergeSortInternal(int[] origin, int left, int right){
if(left >= right){
return;
}
int center = (left + right) / 2;
//对左边数组进行分治递归
mergeSortInternal(origin, left, center);
//对右边数组进行分治递归
mergeSortInternal(origin, center + 1, right);
//对排序好的数组进行合并
merge(origin, left, center, right);
}
private void merge(int[] origin, int left, int center, int right){
int[] tempArr = new int[origin.length];
int tempStartIndex = leftIndex;
int midIndex = center + 1;
int leftIndex = left;
while(leftIndex <= center && midIndex <= right){
if(origin[leftIndex] > origin[midIndex]){
tempArr[tempStartIndex++] = origin[leftIndex++];
}else {
tempArr[tempArrIndex++] = origin[midIndex ++];
}
}
while(leftIndex <= center){
tempArr[tempArrIndex++] = origin[leftIndex++];
}
while(midIndex <= right){
tempArr[tempArrIndex++] = origin[midIndex++];
}
for(int i=left; i<=right; i++){
origin[i] = tempArr[i];
}
}
(2)堆排序(heap sort)
堆排序主要用到二叉树的概念,堆是一棵顺序存储的完全二叉树。
最大堆:其中每个节点都不小于其左右子节点,这样的堆称为最大堆。
最小堆:其中的每个节点都不大于其左右子节点,这样的堆称为最小堆。
一个堆可以用数组表示,某个节点i的左子数为2*i + 1,右子树为2*i+2。某个节点i的父节点为 (i - 1) / 2。
堆排序:
1. 首先要将数组R[0..n]构建成最大堆;
2. 然后将最大堆的R[0]和堆的最后一个交换位置即R[0]和R[n]交换位置;
3. 然后再将R[0..n-1]调整为最大堆,再交换R[0]和R[n-1]的位置;
4. 如此反复,知道交换了R[0]和R[1]为止。
代码如下:
private void maxHeapSort(int[] origin ){
if(origin != null && origin.length > 1){
//构建最大堆
buildMaxHeap(origin);
int swapCount = 1;
while(swapCount < origin.length){
int temp = origin[0];
origin[0] = origin[origin.length - swapCount];
origin[origin.length - swapCount] = temp;
maxHeap(origin, 0, origin.length - swapCount);
swapCount++;
}
}
}
private void buildMaxHeap(int[] origin){
for(int i = origin.length / 2; i>=0; i--){
maxHeap(origin, i, origin.length);
}
}
//将某个树枝节点构建成最大堆,范围在length。
private void maxHeap(int[] origin, int branch, int length){
int leftChild = 2 * branch + 1;
if(leftChild < length){
int swapChildIndex = leftChild;
int rightChild = 2 * branch + 2;
if(rightChild < length && origin[rightChild] > origin[leftChild]){
swapChildIndex = rightChild;
}
if(origin[swapChildIndex] > origin[branch]){
int tmp = origin[swapChildIndex];
origin[swapChildIndex] = origin[branch];
origin[branch] = tmp;
maxHeap(origin, swapChildIndex, length);
}
}
}
(3)快速排序(quick sort)
快速排序也采用了“分治策略”,步骤为:
1. 从数组A[0..n]中找一个数作为基数,以这个基数分为两个子数组,A[0..q]和A[q+1..n],使得A[0..q]都小于等于这个基数,A[q+1..n]都大于等于这个基数。
2. 使用递归对子数组分别进行如上分解。
代码如下:
private void quickSort(int[] origin){
if(origin != null && origin.length > 1){
quick(origin, 0, origin.length-1);
}
}
private void quick(int[] origin, int start, int end){
int startIndex = start;
int endIndex = end;
int baseIndex = start;
int base = origin[baseIndex];
while(endIndex > startIndex){
//首先从后往前遍历找到一个比base小的数然后再互换位置
for(; endIndex > startIndex; endIndex--){
if(base > origin[endIndex]){
int tmp = origin[endIndex];
origin[endIndex] = base;
origin[baseIndex] = tmp;
baseIndex = endIndex;
break;
}
}
//再从前往后遍历找到一个比基数大的数,然后再和基数互换位置
for(; startIndex < endIndex; startIndex++){
if(base < origin[startIndex]){
int tmp = origin[startIndex];
origin[startIndex] = base;
origin[baseIndex] = tmp;
baseIndex = startIndex;
break;
}
}
if(baseIndex - start > 1){
quick(origin, start, baseIndex - 1);
}
if(end - baseIndex > 1){
quick(origin, baseIndex + 1, end);
}
}
}
O(n)的排序算法
计数排序、基数排序及桶排序这三种排序方法都基于对要排序对象的已知限制情况。比如:对一个数组排序,而且此数组的数字在0到1000之间;
对日期或时间进行排序:2017-05-17 21:37:51;
对2017年的高考成绩进行排序。
(1)计数排序
计数排序的精髓就是数一数每一个被排序的数出现了几次。
首先,计数排序要求待排序的所有元素只能取有限并且已知的若干值;
创建一个用于计数的表格,列出所有待排序元素的可能取值,这些可能的取值是已经排序过的,每个取值有一个计数器,计数器的初始值都是0;
对所有的待排序元素进行扫描,在计数表格中找到扫描的值,然后将其计数器加1;
输出的时候只看该数值的计数器是几,就在输出序列中重复输出几遍即可,计数器为0就不输出。
例如有一个数组,已知这个数组中的数都是0..10的。请对此数组进行排序。
//已知是0..10的数字
private void countSort(int[] origin){
int[] countArr = new int[11];
for (int i : origin){
countArr[i] = ++countArr[i];
}
//重新赋值
int index = 0;
for (int i=0; i<countArr.length; i++){
if (countArr[i] > 0){
for (int j=0; j<countArr[i]; j++){
origin[index++] = i;
}
}
}
}
(2)桶排序
桶排序的精髓就是将符合某种条件的待排序元素放在以这个条件命名的一个桶里面,
比如:对某次数学考试的成绩排序,假设成绩都是0到100之间的整数;
1. 首先创建101个考试分数的桶,从0分桶到100分桶,桶里面初始都是空的;
2. 然后对要排序的已有评分的考试试卷,一张一张地去判断,85分的,放到85分的桶里,99分的放在99分的桶里……;
3. 所有试卷都放到对应分数的桶里之后,从100分桶、99分桶、98分桶……里面依次往外取试卷。
(3)基数排序
基数排序的精髓就是按照数字天然的数基去排序,比如整数的“个位、十位、百位、千位……”,时间的“年、月、日、时、分、秒”
1. 基数排序可以从低位开始排,也可以从高位开始排
2. 如果从高位开始排,注意要对高位补0,比如:2017年3月7日,要先写成“2017年03月07日”,或者123要写成0000123,补多少0取决于待排序数最大的有多少位(如果根本不知道最大的有多少位,还是从低位开始排吧)