一、选择排序
基本思想:通过不断的比较数组中待排序部分第一个元素与后面所有元素的大小,找出无序部分的最大值和最小值,并将最大值放在无序部分的最末,最小值放在无序部分的最前面。进而缩小无序部分的长度,直到所有元素都排好序。
如给一个数组{9,4,3,2,1,5,7}。第一次无序部分为整个数组,存放最大值和最小值的地方分别为数组最后一个元素的位置和数组第一个元素的位置,第一个元素既是最大值又是最小值。此时将第一个元素即9后面所有元素和9进行比较。发现比9小的值,不断更新最小值,发现比9大的值,也不断更新最大值,直至遍历整个无序部分。此时最小值为1,最大值为9。值得注意的是,我们是通过不断更新最小值、最大值的下标,最后先交换最小值和存放最小值位置元素,再交换最大值和存放最大值位置元素实现的第一次找最值。此时再进行第二次找最值时,无序部分就会少2个元素,当然,存放最小值和最大值的地方也会变。随着无序部分元素的减少,它们会逐渐靠近。当它们最后相等的时候,选择排序也就完成了。
但是这里会存在一个问题,假如最大值就在存放最小值的位置,那么在交换完最小值和存放最小值位置元素之后,此时最大值就被交换到了最小值所处的位置,再交换最大值和存放最大值位置元素时,此时由于最大值所在下标的位置元素变成了最小值,那么此次交换便不会如我们预期所想。所以值得特别注意的是,再交换完最小值和存放最小值位置元素之后,我们应该判断最大值是否在存放最小值的位置,如果在,让最大值的下标变成最小值的下标。 上边举的例子就有此类问题。在第一次找到最值后,我们发现最大值9在存放最小值的位置,所以当我们交换了1和9之后,应该让maxPos(最大值的下标)等于minPos(最小值的下标)!!!
选择排序完整代码如下:
public class SelectSort {
private int[] arr;
private int len;
public SelectSort(int[] arr) {
this.arr = arr;
this.len = arr.length;
}
//选择排序
//时间复杂度:O(n^2)
public void selectSort() {
int minSpace = 0;//存放最小值的初始位置
int maxSpace = len - 1;//存放最大值的初始位置
while (minSpace < maxSpace) {
int minPos = minSpace;//排序前先将该区间的第一个数当做最小值
int maxPos = minSpace;//排序前先将该区间的第一个数当做最大值
for (int i = minSpace + 1; i <= maxSpace; i++) {
if (arr[i] < arr[minPos]) {
minPos = i;
}
if (arr[i] > arr[maxPos]) {
maxPos = i;
}
}
swap(minPos, minSpace);//交换最小值和存放最小值位置的值
if (maxPos == minSpace) {//如果此时最大值恰好在存放最小值的位置时,经过上一步,将会将最大值交换到最小值所在位置,所以更新最大值所在下标为最小值所在下标
maxPos = minPos;
}
swap(maxPos, maxSpace);
minSpace++;
maxSpace--;
}
}
//交换两个元素的值
public void swap(int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public void print() {
for (int i = 0; i < len; i++) {
System.out.print(arr[i] + " ");
}
System.out.println("");
}
}
稳定性判断:
因为选择排序是从前往后找未排序区间的最大值最小值,所以假如存在两个最大值时,当遇到第一个最大值时便不会再更新最大值下标,而是在一趟遍历完后直接交换最大值和存放最大值位置的值,这么做一定会打乱这两个最大值在排序前的位置,所以选择排序是一种不稳定的排序算法。
二、堆排序
堆的定义:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。
(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4...n/2)
堆总满足下列性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
值得注意的是,升序建大堆,降序建小堆。
堆排序的基本思想:将一个序列建成初堆后,先从最后一个非叶子结点开始向下调整,直到根结点,保证其满足堆的性质;此时大堆变建成了。然后交换根结点和最后一个结点的元素值,同时让序列的元素个数减1,并重新对根结点向下调整;再交换根结点和最后一个结点的元素值,同时让序列的元素个数减1,并重新对根结点向下调整。重复上述过程,直到遍历整个序列。
有的人可能很疑惑,为什么要从最后一个非叶节点开始向下调整呢?我们知道叶节点表示其左右孩子节点为空或无孩子结点,那么对叶节点做向下调整的时候很明显其满足堆的性质---其data值大于等于左右孩子结点的data值。所以,我们一般从最后一个非叶节点开始向下调整,直到根结点。
那么,如何向下调整使其满足堆的性质呢?
都知道是从最后一个非叶节点开始向下调整,使得其data值大于等于左右孩子结点的data值,才满足大堆的性质。所以,
第一步:判断所要调整的结点是否有左右孩子。前面提到过堆总是一颗完全二叉树,所以当判断了左孩子不在堆中时,右孩子一定也不在。
第二步:找出左右孩子中data值较大的结点 ,比较其data值与父节点的data值:如果前者大于后者,交换data值 。否则,说明该结点已经满足堆的性质,那么继续对下一个需要向下调整 的结点做向下调整,直到根结点。
第三步:对左右孩子中data值较大的结点继续向下调整,即重复上述步骤。
如何确定最后一个非叶节点的下标:我们知道数组是从下标0开始存储元素的,那么在已知数组大小的情况下,如数组有size个元素,那么最后一个元素的下标一定是size-1。而最后一个非叶节点 一定是最后一个元素的父节点,那么它的下标便可以确定了,即(size-1-1)/2。
如给定一个数组{2,5,7,3,9};建初堆时如下所示
2
/ \
5 7
/ \
3 9
可以看出该数组共有5个元素,所以size=5。 最后一个元素的下标为size-1=4,所以最后一个非叶节点的下标为(size-1-1)/2=(4-1)/2=1。 我们从5所在结点开始做向下调整。因为右孩子的data值9大于其父节点data值5,交换对应值。此时再对 右孩子做向下调整,因为其左右孩子(NULL)不在数组中,所以此次对最后一个非叶节点的向下调整结束(以最后一个非叶节点为根结点的堆已满足大堆的性质)。调整结果如下
2
/ \
9 7
/ \
3 5
接着对array[0],即2所在结点(根结点)做向下调整。可以看出此时根结点的左孩子的data值9大于其data值2,交换对应值。交换后的堆如下
9
/ \
2 7
/ \
3 5
继续对根结点的左孩子做向下调整。因为其data值2小于其右孩子的data值5,交换对应值。此时再对其右孩子做向下调整,因为其左右孩子(NULL)不在数组中 ,所以此次对根结点的向下调整结束。调整结果如下
9
/ \
5 7
/ \
3 2
到此,大堆便建成了。
接着便是交换根结点的data值和最后一个结点的data值,同时在size-1的情况下每次对根结点做向下调整 ,直到数组中没有元素了,这样便可以依次取出最大值,次大值,·····,直到最小值。过程如下
(交换2和9) (size-1后对根结点做向下调整)
2 7
/ \ / \
5 7 5 2
/ \ /
3 9 3
得到了最大值9
(交换7和3) (size-1后对根结点做向下调整)
3 5
/ \ / \
5 2 3 2
/
7
得到了次大值7
(交换5和2) (size-1后对根结点做向下调整)
2 3
/ \ /
3 5 2
得到了5
(交换2和3) (size-1后对根结点做向下调整)
2 2
/
3
得到了3
(交换2和2) 此时size-1=0
2
得到了2
实现代码如下:
public class HeapSort {
private int[] arr;
private int len;
public HeapSort(int[] arr) {
this.arr = arr;
this.len = arr.length;
}
//堆排序
//时间复杂度:O(nlgn)
public void heapSort() {
//1.从最后一个非叶节点开始做向下调整,直到根节点
for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
adjustDown(i, len);
}
//2.交换根节点的值和最后一个节点的值,对根节点做向下调整,同时让节点个数-1,重复上述过程,直到节点个数为0
for (int i = 0; i < len; i++) {
swap(0, len - 1 - i);//交换根节点的值和最后一个节点的值
adjustDown(0, len - 1 - i);//在节点个数减-的情况下对根节点做向下调整
}
}
private void swap(int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
private void adjustDown(int root, int len) {
int left = 2 * root + 1;//root所在节点的左孩子节点下标
int right = 2 * root + 2;//root所在节点的右孩子节点下标
if (left < len) {//保证左孩子所在节点的下标一定在数组中
int maxChild = left;//先将左孩子节点值作为左右孩子节点值的较大值
if (right < len && arr[right] > arr[left]) {//保证右孩子所在节点下标也在数组中,且当右孩子节点值大于左孩子节点值时更新较大值节点下标
maxChild = right;
}
if (arr[root] >= arr[maxChild]) {//比较父节点和较大孩子节点的值,如果父节点值大于等于较大孩子节点值,则对该节点(父节点)的向下调整结束
return;
} else {//否则,交换父节点值和较大孩子节点值,并对较大孩子节点做向下调整(保证以父节点为根节点的二叉树满足堆的性质)
swap(root, maxChild);
adjustDown(maxChild, len);
}
}
}
public void print() {
for (int i = 0; i < len; i++) {
System.out.print(arr[i] + " ");
}
System.out.println("");
}
}
稳定性判断:堆排序是一种不稳定的排序算法。