简单算法:
冒泡排序:
是一种交换排序,基本思想是:俩俩比较相邻记录的关键字,如有反序则交换,直到没有反序的记录为止。
实现(这里从后往前确定最大数进行升序排序):
public void bubble(int[] array){
for(int i = 0;i < array.length;i++){
for(int j = 0;j < array.length - i - 1;j++){
if(array[j + 1] < array[j]){
swap(array,j,j + 1);
}
}
}
}
public void swap(int[] array,int a,int b){
int cur = array[a];
array[a] = array[b];
array[b] = cur;
}
优化:
假如我们要对 2,1,3,4,5,6,7进行升序排序,实际上只发生了对1和2进行交换的操作,剩下所有数字都是有序的,但是上述代码还对已经有序的数组进行比较,做了大量无用功。所以我们可以利用这一点对冒泡排序进行优化:设置一个标志位。如下所示:
public void bubble(int[] array){
for(int i = 0;i < array.length;i++){
boolean flag = true;//每一遍循环前设置为true
for(int j = 0;j < array.length - i - 1;j++){
if(array[j + 1] < array[j]){
swap(array,j,j + 1);
flag = false;//如果发生交换设置为false
}
}
//标志位为true说明这一遍循环没有发生交换,即为有序
//标志位为false则继续进行排序
if(flag){
break;
}
}
}
public void swap(int[] array,int a,int b){
int cur = array[a];
array[a] = array[b];
array[b] = cur;
}
复杂度分析:最好情况:数组本来就有序,则在优化后只在i = 0时进行了n - 1次比较就通过标志位结束排序,此时时间复杂度可达到: O(n)
最坏情况:数组逆序 需要进行1+2+3+......+n - 1次比较,时间复杂度:O(n^2)
因此冒泡排序时间复杂度为O(n^2)
稳定性:稳定排序
空间复杂度:O(1)
简单选择排序
:从n - i + 1个数据中通过比较选出关键字最大或最小的记录并和第i 个记录交换(1<=i<=n). i从1到n逐次变化
public void select(int[] array){
for(int i = 0;i < array.length;i++){
int flag = i;
for(int j = i + 1;j < array.length;j++){
if(array[j] < array[flag]){
flag = j;
}
}
swap(array,flag,i);
}
}
复杂度分析:
时间复杂度:无论情况好坏都需要进行1+2+3+......+n - 1次比较,复杂度为O(n^2)
空间复杂度:O(1)
稳定性:不稳定排序(在与第i个记录交换时可能会使同样大的数的顺序打乱)
直接插入排序:
将一个记录插入到已经有序的记录中得到一个新的,记录数加1的有序表
public void chaRu(int[] array){
for(int i = 1;i < array.length;i++){
int j = i - 1;
while(j >= 0){
if(array[j] <= array[j + 1]){
break;
}else{
swap(array,j,j + 1);
}
j--;
}
}
}
复杂度分析:
时间复杂度:最好情况:数组基本有序:复杂度O(n),最坏情况数组逆序 复杂度为:O(n^2)
则直接插入排序的复杂度为O(n^2)
空间复杂度:O(1)
稳定性:稳定排序
改进算法:
希尔排序
又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录以这个整数为间隔分成多个组, 所有距离为这个整数的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当整数到达1时,所有记录在统一组内排好序。
public void xiEr(int[] array){
int count = array.length / 2;
while(count > 0){
for(int i = count;i < array.length;i++){
int j = i - count;
int flag = array[i];
while(j >= 0){
if(array[j] <= array[j + count]){
break;
}else{
array[j + count] = array[j];
}
j -= count;
}
array[j + count] = flag;
}
count /= 2;
}
}
复杂度分析:
时间复杂度:取决于增量的选取,并且至今没有一个最好的增量确定方式,但综合下来其时间复杂度一定是突破了O(n^2)的,在上述代码中我们的增量取了数组长度的一半并依次减半,这时候的时间复杂度可以达到O(n^1.25) - O(1.6*n^1.25)
空间复杂度:O(1)
稳定性:不稳定排序
堆排序
将待排序的序列构建成一个大根堆或一个小根堆(升序构建大根堆,降序构建小根堆),则此时堆顶元素就是我们这个序列的最大或最小值,将其与对末尾的元素交换,则此时有序序列的一个元素就确定下来了,再对堆顶元素进行向下调整使其成为一个大根堆或小根堆(注意这里向下调整过程中要去掉上一次我们交换到末尾的元素),重复上述步骤,直到堆只剩下一个元素。
public void heap(int[] array){
creadHeap(array);
int count = array.length - 1;
while(count > 0){
swap(array,0,count);
adjustDown(array , 0,count - 1);
count--;
}
}
private void creadHeap(int[] array){
int parent = (array.length - 1 - 1) / 2;
while(parent >= 0){
adjustDown(array,parent,array.length - 1);
parent--;
}
}
private void adjustDown(int[] array,int parent,int end){
int son = parent * 2 + 1;
while(son <= end){
if(son + 1 <= end && array[son] < array[son + 1]){
son++;
}
if(array[parent] < array[son]){
swap(array,parent,son);
parent = son;
son = parent * 2 + 1;
}else{
break;
}
}
}
复杂度分析:
时间复杂度:在外面刚开始构建堆的时候时间复杂度为O(n),之后进行向下调整的时间复杂度为堆的深度O(logi ),i为此时堆的元素的个数,需要进行n - 1次向下调整,
综合一下为:O(n+(logi)*(n-1))
则时间复杂度为:O(nlogn);
空间复杂度:O(1)
稳定性:不稳定
归并排序
将n个记录按照平分的方式划分为2个子序列,再对这俩个子序列按照同样的方式进行划分,直到每个子序列的长度为1,然后俩俩归并得到一个长度为2的子序列,继续进行归并,直到完全合并。即:使每个子序列有序,再使子序列段间有序,将两个有序表合并成一个有序表。
递归实现:
public void mergeSort(int[] array){
merge(array,0,array.length - 1);
}
private void merge(int[] array,int left,int right){
if(left >= right){
return;
}
int mid = (right + left) / 2;
merge(array,left,mid);
merge(array,mid + 1,right);
combine(array,mid,left,right);
}
private void combine(int[] array,int mid,int left,int right){
int s1 = left;
int s2 = mid + 1;
int[] cur = new int[right - left + 1];
int count = 0;
while(s1 <= mid && s2 <= right){
if(array[s2] < array[s1]){
cur[count++] = array[s2++];
}else{
cur[count++] = array[s1++];
}
}
while(s1 <= mid){
cur[count++] = array[s1++];
}
while(s2 <= right){
cur[count++] = array[s2++];
}
for(int i = 0;i < count;i++){
array[i + left] = cur[i];
}
}
非递归实现:
public void mergeSortNor(int[] array) {
int gap = 1;
while(gap < array.length) {
for (int i = 0;i < array.length;i += 2*gap) {
int mid = i + gap - 1;
int left = i;
int right = mid + gap;
if(mid >= array.length){
mid = array.length - 2;
}
if(right >= array.length){
right = array.length - 1;
}
combine(array,mid,left,right);
}
gap = gap * 2;
}
}
private void combine(int[] array,int mid,int left,int right){
int s1 = left;
int s2 = mid + 1;
int[] cur = new int[right - left + 1];
int count = 0;
while(s1 <= mid && s2 <= right){
if(array[s2] < array[s1]){
cur[count++] = array[s2++];
}else{
cur[count++] = array[s1++];
}
}
while(s1 <= mid){
cur[count++] = array[s1++];
}
while(s2 <= right){
cur[count++] = array[s2++];
}
for(int i = 0;i < count;i++){
array[i + left] = cur[i];
}
}
复杂度分析:
时间复杂度:每一次二路合并都要将所有记录扫描一遍,复杂度为O(n),整个归并排序要进行log(n)(将归并排序类比成完全二叉树,次数就相当于完全二叉树的深度),则时间复杂度为O(n*logn)
空间复杂度:递归实现:每一次进行二路合并都要申请一个子序列2倍大小的存储空间O(n)
同时,如果进行递归需要深度为logn的栈空间 综合则为:O(n+logn)
非递归实现:O(n)
因此:基于空间的角度来说,我们应该尽量考虑使用非递归方法实现归并排序
快速排序
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
Hoare方法
默认最左边元素left为基准值,设置俩个标记left和right分别从前往后和从后往前进行遍历,只要left遇到大于基准值的元素就停下来,同理,right遇到小于基准值的元素停下来,然后将left和right所对应的元素交换,重复上述过程直到left和right相遇。将基准值与left所对应元素交换。此时基准值左边均为小于等于它的元素,右边均为大于等于它的元素,即左右子序列。在左右子序列中重复上述过程,直到子序列只有一个元素。
public void quickSort(int[] array) {
quickSortHelper(array,0,array.length - 1);
private void quickSortHelper(int[] array,int left,int right){
if(left >= right){
return;
}
int div = Sort(array,left,right);
quickSortHelper(array,left,div - 1);
quickSortHelper(array,div + 1,right);
}
private int Sort(int[] array,int left,int right){
obtainKey(array,left,right);
int count = left;
int flag = array[left];
while(left < right){
while(left < right && array[right] >= flag){//这里必须加等号,不加等号就会落入一个交换的循环
right--;
}
//这里必须先从右边减,如果先从左边加,最后left和right相遇的地方的值是比基准大的数
while(left < right && array[left] <= flag){
left++;
}
swap(array,left,right);
}
swap(array,left,count);
return left;
}
挖坑法
默认最左边元素为基准值,将基准值存储在一个变量中,right先从右到左 递减找到一个比基准值小的数覆盖到left位置上,left++直到找到一个比基准值大的数覆盖到right位置上,重复上述操作,直到left和right相遇,此时发现left和right相遇的位置为空,将保存的基准值移到这个位置上,其左右分别为其子序列。
public void quickSort1(int[] array){
quickSort1Helper(array,0,array.length - 1);
}
public void quickSort1Helper(int[] array,int left,int right){
if(left >= right){
return;
}
int middle = middle(array,left,right);
quickSort1Helper(array,left,middle - 1);
quickSort1Helper(array,middle + 1,right);
}
public int middle(int[] array,int left,int right){
int flag = array[left];
while(left < right){
while(left < right && array[right] >= flag){
right--;
}
array[left] = array[right];
while(left < right && array[left] <= flag){
left++;
}
array[right] = array[left];
}
array[left] = flag;
return left;
}
前后指针法
public void quickSort2(int[] array){
quickSort2Helper(array,0,array.length - 1);
}
public void quickSort2Helper(int[] array,int left,int right){
if(left >= right){
return;
}
int middle = middle2(array,left,right);
quickSort2Helper(array,left,middle - 1);
quickSort2Helper(array,middle + 1,right);
}
public int middle2(int[] array,int left,int right){
int cur = left + 1;
int temp = left;
int flag = array[left];
while(cur <= right){
if(array[cur] <= flag && array[++temp] != array[cur]){
swap(array,cur,temp);
}
cur++;
}
//为什么temp这里交换
//t因为emp是最后一个小于等于flag的数
swap(array,temp,left);
return temp;
}
快速排序的优化
三数取中法(优化对基准数的选择,使快速排序尽可能成为一个完全二叉树)
从数组前,后,中,三个位置取得三个数,比较得到中间的那个数,交换到最左边成为基准数
private void obtainKey(int[] array,int left,int right){
int middle = (left + right) / 2;
if(array[left] > array[right]){
if(array[middle] < array[right]){
swap(array,left,right);
} else if (array[middle] > array[left]) {
return;
}else{
swap(array,middle,left);
}
} else {
if(array[middle] > array[right]){
swap(array,right,left);
} else if (array[middle] < array[left]) {
return;
}else{
swap(array,middle,left);
}
}
尾递归优化
递归是要消耗方法栈的资源的,栈的大小是有限的,如果快排时序列的划分不平衡,且数据量较大,可能会造成栈溢出问题。
并且快排进行到最后是划分序列最多的时候,这时会消耗大量的栈空间。
如果我们能在快排进行到后面时使用其它排序方法,就能节省大量栈空间。
代码如下(这里采用插入排序对尾递归进行优化)
public void quickSort(int[] array) {
quickSortHelper2(array,0,array.length - 1);
}
//到树底时采用插入排序(减少递归,避免栈溢出)
private void quickSortHelper2(int[] array,int left,int right){
if(left >= right){
return;
}
if(right - left <= 3){
chaRuhelpQuickSort(array,left,right);
return;
}
int div = Sort(array,left,right);
quickSortHelper2(array,left,div - 1);
quickSortHelper2(array,div + 1,right);
}
private void chaRuhelpQuickSort(int[] array,int left,int right){
for(int i = left + 1;i <= right;i++){
int j = i - 1;
while(j >= left){
if(array[j] <= array[j + 1]){
break;
}else{
swap(array,j,j + 1);
}
j--;
}
}
}
快速排序的非递归实现
尽管我们对快速排序进行了优化,但当数据量很大时,快排还是会进行大量递归操作,产生栈溢出问题。这时我们可以利用栈实现快排的非递归方法:
public void quickSortNorRecursion(int[] array){
Stack<Integer> stack = new Stack<>();
if(array.length < 2){
return;
}
stack.push(0);
stack.push(array.length - 1);
while(!stack.isEmpty()){
int right = stack.pop();
int left = stack.pop();
int mid = quickSortNorRecursionhelper(array,left,right);
if(mid - 1 > left){
stack.push(left);
stack.push(mid - 1);
}
if(mid + 1 < right){
stack.push(mid + 1);
stack.push(right);
}
}
}
public int quickSortNorRecursionhelper(int[] array,int left,int right){
obtainKey(array,left,right);
int count = left;
int flag = array[left];
while(left < right){
while(left < right && array[right] >= flag){//这里必须加等号,不加等号就会落入一个交换的循环
right--;
}
//这里必须先从右边减,如果先从左边加,最后left和right相遇的地方的值是比基准大的数
while(left < right && array[left] <= flag){
left++;
}
swap(array,left,right);
}
swap(array,left,count);
return left;
}
总结:
从算法的简单性来看,我们将7种算法分为以下两类。 .
简单算法:冒泡、简单选择、直接插入。
改进算法:希尔、堆、归并、快速。
从平均情况来看,显然最后3种改进算法要胜过希尔排序,并远远胜过前3种简单算法。
从最好情况看,反而冒泡和直接插入排序要更胜一筹,也就是说,如果你的待排序序列总是基本有序,反而不应该考虑4种复杂的改进算法。
从最坏情况看,堆排序与归并排序又强过快速排序以及其他简单排序。 从这三组时间复杂度的数据对比中,我们可以得出这样一个认识。堆排序和归并排序就像两个参加奥数考试的优等生,心理素质强,发挥稳定。而快速排序像是很情绪化 的天才,心情好时表现极佳,碰到较糟糕的环境会变得差强人意。但是他们如果都来比 赛计算个位数的加减法,它们反而算不过成绩极普通的冒泡和直接插入。
从空间复杂度来说,归并排序强调要马跑得快,就得给马吃个饱。快速排序也有相 应的空间要求,反而堆排序等却都是少量索取,大量付出,对空间要求是O(1)。如果执行算法的软件所处的环境非常在乎内存使用量的多少时,选择归并排序和快速排序就不是一个较好的决策了。
从稳定性来看,归并排序独占鳌头,我们前面也说过,对于非常在乎排序稳定性的应用中,归并排序是个好算法。
从待排序记录的个数上来说,待排序的个数n越小,采用简单排序方法越合适。反之,n越大,采用改进排序方法越合适。这也就是我们为什么对快速排序优化时,增加了 一个阈值,低于阈值时换作直接插入排序的原因。(节选自《大话数据结构》)
-