一、排序算法概览
1.1 思维导图
最近在准备面试,所以把经典的排序算法再敲了一下,然后做了个思维导图,梳理记录一下重点。
1.2 时空复杂度比较
图片来自网络,侵删
二、算法实现
2.1 插入排序
2.1.1 直接插入排序
- 基本思想:每次将一个待排序的记录,按其关键字大小与前面已排序子序列进行比较,找到插入位置,然后插入位置后元素向后顺移,然后将该关键字插入到前面留出的空位中。
- 代码实现
public void DirectInsertSort(int []array) {
for(int i=1; i<array.length; i++) {
if(array[i] < array[i-1]) { //当检测到逆序元素
int tmp = array[i];
int j = i-1;
while(j>=0 && array[j]>tmp) { //边比较边插入
array[j+1] = array[j];
j--;
}
array[j+1] = tmp;
}
}
}
- 稳定性:稳定。每次插入元素总是从后方向前比较再移动,所以不会出现相同元素发生变化的情况。
2.1.2 折半插入排序
- 基本思想:直接插入排序的升级版。直接排序总是变比较边移动元素,折半插入排序将比较和移动操作分离出来,即先折半查找出元素的待插入位置然后再统一地移动插入位置后的所有元素。
- 代码实现
public void HalfInsertSort(int []array) {
int i,j,low,mid,high;
for(i=1; i<array.length; i++) {
int tmp = array[i];
low = 0;
high = i-1;
//折半查找当前元素在已排序序列中的插入位置
while(low <= high) {
mid = (low + high)/2;
if(array[mid] > tmp)
high = mid-1;
else
low = mid+1;
}
for(j=i-1; j>=high+1; j--) //找出待插入的位置 "high+1"
array[j+1] = array[j];
array[j+1] = tmp;
}
}
- 稳定性:稳定。
- 折半插入排序仅仅是为了减少比较元素的次数,该比较次数与待排序表的初始状态无关,仅取决于表中元素个数 n。元素的移动次数没有改变,它依赖于待排序表的初始状态
2.1.3 Shell 排序
- 直接插入排序算法适用于基本有序的排序表和数据量不大的排序表。基于这两点,D.L.Shell 提出了希尔排序,又称为缩小增量排序。
- 基本思想:先将待排序表分割成若干个形如L[i,i+d,i+2d,…,i+kd]的“特殊字表”,分别进行直接插入排序,当整个表中元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
- 代码实现
public void ShellSort(int []array) {
int dk = array.length/2; //设置初始步长
//当步长为1时,即对基本有序的待排序表进行最后一次直接插入排序
while(dk >= 1) {
for(int i=dk+1; i<array.length; i++) {
if(array[i] < array[i-dk]) {
int tmp = array[i];
int j = i-dk;
while(j>=0 && array[j]>tmp) {
array[j+dk] = array[j];
j -= dk;
}
array[j+dk] = tmp;
}
}
dk /= 2;
}
}
- 稳定性:不稳定。当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序。
2.1.4 插入类排序总结
- 插入类排序最重要的思想是:依次将待排序表中元素,与已排序子序列进行比较,若为逆序,则将已排序子序列元素向后顺移,直到找到插入位置,将该待排序序列元素插入到已排序中。
2.2 交换排序
2.2.1 冒泡排序
- 基本思想:假设待排序表长为n,从后往前(或从前往后)两两比较元素的值,若为逆序(即A[i-1]>A[i])则交换它们,直到序列比较完。我们称它为一趟冒泡,结果将最小的元素交换到待排序的第一个(关键字最小的元素如气泡一样逐渐向上“漂浮”直至“水面”,这就是冒泡排序名字由来)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,待排序列减少一个元素,每趟冒泡的结果把序列中的最小元素放到了序列的最终位置。
- 代码实现
public void BubbleSort(int []array) {
//表示本趟冒泡是否发生交换的标志,
//若当前待排序表已经有序,可跳出循环,减少比较次数
boolean flag = false;
for(int i=0; i<array.length-1; i++) {
for(int j=array.length-1; j>i; j--) {
if(array[j] < array[j-1]) { //若为逆序
swap(array,j,j-1);
flag = true;
}
}
if(flag == false)
return ; //本趟遍历后没有发生交换,说明表已经有序
}
}
- 稳定性:稳定。当 i>j 且A[i]=A[j]时,不会交换两个元素,从而冒泡排序是一个稳定的排序算法。
2.2.2 快速排序
- 基本思想:快速排序是冒泡排序的一种改进。基本思想是基于分治法的:在待排序表L[1…n]中任取一个元素pivot作为基准通过一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n]。使得L[1…k-1]中所有元素小于pivot,L[k+1…n]中所有元素大于或等于 pivot,则 pivot 放在了其最终位置L(k)上,这个过程称为一趟快速排序。而后重复上诉过程,直至每部分内只有一个元素或为空为止,即所有元素放在了其最终位置上。
- 代码实现
public void QuickSort(int []array,int low,int high) {
if(low < high) {
int pivot = Partion(array,low,high);
QuickSort(array,low,pivot-1);
QuickSort(array,pivot+1,high);
}
}
//重点理解掌握划分算法
public int Partion(int []array,int low,int high) {
int pivot = array[low];
while(low < high) {
//从待排序表的末端与基准进行比较。若大,则 high 前移
while(low < high && array[high] >= pivot)
high --;
//若小,则将当前 high 所指元素移动到 low 处
array[low] = array[high];
//从待排序的首端与基准进行比较。若小,则 low 后移
while(low < high && array[low] <= pivot)
low ++;
//若大,则将当前 low 所指元素移动到 high 处
array[high] = array[low];
}
//划分结束,将基准元素填入到 low 处
array[low] = pivot;
return low;
}
- 空间复杂度:由于快速排序是递归的,需要借助一个递归工作栈来保存每一层递归调用的必要信息,其容量应与递归调用的最大深度一致。因而最坏情况为O(n),平均情况为O(logn)。
- 时间复杂度:快排的运行时间与划分是否对称有关,而后者又与具体使用的划分算法有关。若初始排序表基本有序或基本逆序时就得到最坏情况下的时间复杂度O(n^2)。
- 稳定性:不稳定。在划分算法中,若右端区间存在两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置就会发生变化。
2.2.3 交换类排序总结
- 所谓交换,就是根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。
- 冒泡排序是最简单的交换排序,它不断进行交换操作,直至排序完毕。
- 快速排序,运用分治法的思想,每次将待排序表按设定的基准值划分成两个待排序子表。每一次划分操作后,基准值的位置(在排序表中的最终位置)被确定。运用递归技术,重复进行这一操作,直到所有元素都被划分完毕,所有基准值的位置就被唯一确定。而划分过程中,也就是元素不断交换位置的过程,所以把快速排序归到交换排序中。
2.3 选择排序
2.3.1 简单选择排序
- 基本思想:假设排序表为L[1…n],第 i 趟排序即从L[i…n]中选择关键字最小的元素与L(i)交换。每一趟排序可以确定一个元素的最终位置。
- 代码实现
public void SelectSort(int []array) {
for(int i=0; i<array.length-1; i++) {
int min = i;
for(int j=i+1; j<array.length; j++) {
if(array[min] > array[j])
min = j;
}
if(min != i)
Swap(array,i,min);
}
}
- 时间复杂度:元素移动次数(交换)很少,最好情况是移动0次,此时对应的表已经有序。但元素的间的比较次数与序列的初始状态无关,始终是n(n-1)/2次,故时间复杂度始终为O(n^2)
- 稳定性:不稳定。假设待排序表为[2,2,1],经过选择排序后为[1,2,2]。
2.3.2 堆排序
- 基础概念补充:堆排序用堆这一数据结构,每次选择出堆顶(大顶堆或小顶堆)元素,以选择出的堆顶元素为序完成排序。
大顶堆:父亲结点元素值大于任一孩子结点
小顶堆:父亲结点元素值小于任一孩子结点
堆是 完全二叉树,可以用数组存储。若父亲结点的index=i,则左孩子结点为2i+1;右孩子结点为2i+2
- 基本思想
此堆排序以升序对整型数进行排序,故构建的是大顶堆。
heapsort:
1.首先将初始堆初始化为大顶堆(buildHeap)
2.在逻辑上将堆顶元素删除,具体做法是将堆顶元素与堆底末尾元素互换,当前可操作堆结点-1(swap)
3.接着对剩下的部分合法大顶堆(堆顶结点不合法)进行调整(heapify) - 代码实现
void swap(int []tree,int i,int j){
int tmp=tree[i];
tree[i]=tree[j];
tree[j]=tmp;
}
void heapify(int []tree,int n,int i){ //从index=i的结点开始,将当前部分合法大顶堆调整为大顶堆
if(i>=n)
return ;
int c1 = 2*i+1;
int c2 = 2*i+2;
int max = i;
if(c1 < n && tree[c1] > tree[max])
max = c1;
if(c2 < n && tree[c2] >tree[max])
max = c2;
if(max != i){
swap(tree,max,i);
heapify(tree,n,max);
}
}
void buildHeap(int []tree,int n){ //利用heapify方法将初始堆初始化为大顶堆
int last_node = n-1; //最后一个叶子结点的index
int parent_node = (last_node-1)/2; //计算最后一个叶子结点的父亲结点,并以此开始向上做堆化操作
for(int i=parent_node;i>=0;i--){
heapify(tree,n,i);
}
}
void heapSort(int []tree,int n){
buildHeap(tree,n);
for(int i=n-1;i>0;i--){
swap(tree,i,0);
heapify(tree,i,0);
}
}
- 时间复杂度:堆排序的时间复杂度,主要在初始化堆过程和每次选取最大数后重建堆的过程。
1.初始化建堆过程的时间复杂度为:O(n)
2.更改堆顶元素后重建堆的时间复杂度为:O(nlogn)。循环n-1次,每次都是从根结点往下循环查找。由于每次更改堆后,堆中结点数-1,故第i次重建过程查找用时为log(n-i),总用时(数学计算)为O(nlogn)
3.综上所述,时间复杂度为:O(nlogn) - 空间复杂度:堆排序为就地排序,故为O(1)
- 稳定性:不稳定。结合完全二叉树特性,堆重建后可能会导致次序发生改变(可画图理解)
2.3.3 选择类排序总结
2.4 其它
2.4.1 归并排序
- 基本思想
“归并”的含义是将两个或两个以上的有序表合成一个新的有序表
merge()
的功能是将前后相邻的两个有序表归并为一个新的有序表 - 代码实现
public void mergeSort(int[] array, int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
mergeSort(array, low, mid);
mergeSort(array, mid + 1, high);
merge(array, low, mid, high);
}
}
private void merge(int[] array, int low, int mid, int high) {
int[] temp = new int[array.length];
if (high + 1 - low >= 0)
System.arraycopy(array, low, temp, low, high + 1 - low);
int i = low, j = mid + 1, k = low;
while (i <= mid && j <= high) {
if (temp[i] <= temp[j]) {
array[k++] = temp[i++];
} else {
array[k++] = temp[j++];
}
}
while (i <= mid) array[k++] = temp[i++];
while (j <= high) array[k++] = temp[j++];
}
- 时间复杂度:每趟归并的时间复杂度为 O(n),共需进行 logn 趟归并,所以算法的时间复杂度为 O(nlogn)
- 空间复杂度:
merge()
操作中,辅助空间刚好 n 个单元,所以算法的空间复杂度为 O(n) - 稳定性:
merge()
操作中不会改变相同关键字记录的相对次序,所以2路归并排序算法是稳定的排序算法
2.4.2 基数排序
…未完待续…
- 基本思想
- 代码实现