堆排序
时间复杂度:nlogn
堆的条件
- 完全二叉树(了解可以看笔者另外一篇关于二叉树的解读)
二叉树的概念 - 父节点大于子节点 or 小于子节点
大于子节点的堆,叫大根堆,反之则为小根堆
堆排序的核心就两步,这里以大根堆为例子
第一步,建堆
第二步,排序
建堆
- 我们从最后一个节点的父节点开始,从右往左,然后往上的顺序,这样子可以保证,下边的树,都是堆。
- 父节点 子节点 中找到最大值,然后交换
这里需要注意的是,假设我们当前节点为i,这里是以0开始的数组
parent = (i - 1) / 2
c1 = 2 * i + 1
c2 = 2 * i + 2
排序
建完堆后,根节点就是最大值,与最后一个节点交换,然后下一趟排序的范围就少一个值
需要注意的是,我们这里是从最后一个节点的父节点开始的
为什么要从这里开始呢,实际上就是为了省下一些没必要的操作,因为叶子节点没必要进行heapify
public static void heapSort1(int[] data)
{
//排序的次数等于长度 - 1,例如长度为5,那么只用四次就可以找到4个最大值,那么最后一个值,一定比其他四个值小
for(int i = 0; i < data.length - 1; i++)
{
heapify1(data,data.length - i - 1); //建堆,这里有边界,每进行一次建堆,范围就得小一个值,那么刚好可以用趟数减去
swap(data,0,data.length - i - 1); //交换最后一个值
}
}
private static void heapify1(int[] data, int right) {
//找到叶子节点
int parent = (right - 1) / 2;
//开始做循环,循环结束的条件就是到根节点
while(parent >= 0)
{
int c1 = parent * 2 + 1; //左节点
int c2 = parent * 2 + 2; //右节点
int max = parent;
if(c1 <= right && data[c1] > data[max]) //这里需要注意左右节点的越界问题
{
max = c1;
}
if(c2 <= right && data[c2] > data[max])
{
max = c2;
}
swap(data,max,parent);
parent--;
}
}
补充
堆实际上就是个线性结构,这里再使用链表来表示堆,它的基本运算有三个
- void push(E e) 插入元素
- E pop() 删除一个元素并返回该元素
- boolean empty() 判断是否为空
现在,为了简单,假设元素类型为RecType, 用 R[1…n]存放堆中的元素
插入算法
public void push(RecType e)
{
n++;
R[n] = e;
if(n == 1) return;
int j = n, i = j / 2;
while(true)
{
if(R[j].key > R[i].key)
{
swap(i.j);
}
ifi(i == 1) break;
j = i; i = j / 2;
}
}
这里的算法,其实十分简单,我们插入一个元素,只用把插入的地方的父节点,一直到根节点,这样就实现了。
这里的插入方法,也可以使用到堆排序的方法里边
public static void heapSort1(int[] data)
{
//排序的次数等于长度 - 1,例如长度为5,那么只用四次就可以找到4个最大值,那么最后一个值,一定比其他四个值小
for(int i = 0; i < data.length - 1; i++)
{
heapify2(data,data.length - i - 1); //建堆,这里有边界,每进行一次建堆,范围就得小一个值,那么刚好可以用趟数减去
swap(data,0,data.length - i - 1); //交换最后一个值
}
}
private static void heapify2(int[] data, int right)
{
for(int n = 1; n <= right; n++)
{
int j = n; //当前节点
int i = (j - 1) / 2; //父节点
while(true)
{
if(data[j] > data[i]) //如果大于的话,就交换
{
swap(data,j,i);
}
if(i == 0) break; //到达跟节点,结束循环
j = i; //j 指向父节点,继续往上修正堆
i = (j - 1) / 2; //i 指向j的父节点
}
}
}
归并排序
自底向上的二路归并排序
private static void MergeSort(int[] data,int low,int right)
{
if(low < right)
{
int mid = (low + right) / 2; //中间节点
MergeSort(data,low,mid); //对左边开始排序
MergeSort(data,mid + 1,right); //对右边开始排序
Merge(data,low,mid,right); //合并
}
}
public static void Merge(int[] data,int low,int mid,int high)
{
//需要注意的是,这个方法的前提是,low到mid是有序的,mid + 1到high也是有序的
int[] newData = new int[high - low + 1]; //新的数组
int i = low; //i指向low
int j = mid + 1; //j指向第二段的第一个开头
int k = 0; //新数组的指针
while(i <= mid && j <= high) //两边都要做限制
{
if(data[i] <= data[j])
{
newData[k++] = data[i++];
} else {
newData[k++] = data[j++];
}
}
while(i <= mid)
{
newData[k++] = data[i++];
}
while(j <= high)
{
newData[k++] = data[j++];
}
for(int n = 0,m = low; n < newData.length; n++,m++)
{
data[m] = newData[n]; //复制回数组
}
}
归并排序时间复杂度
对于归并排序,主要是,两次分,一次合
T(n) = 2T(n / 2) + O(n) (n > 1)
我们使用主定理法
带入公式,就可以得到答案
快速排序
public static void qSort(int[] data,int l, int r)
{
if(l < r)
{
int q = partition(data,l,r);
qSort(data,l,q - 1);
qSort(data,q + 1, r);
}
}
private static int partition(int[] data, int l, int r) {
int i = l;
int j = r + 1;
int x = data[l];
while(true)
{
while(data[++i] < x && i < r); //这里小于r是因为,这里++i边界其实是r - 1,如果小于等于r的话,就会数组越界
while(data[--j] > x); //这里不用作数组越界是因为,j到最左边,到头部元素的话,不可能再往左走,所以就可以省略
if(i >= j)break;
swap(data,i,j);
}
//交换data[j]和头部元素
data[l] = data[j];
data[j] = x;
return j;
}
快速排序时间复杂度
最坏情况就是,例如 9 8 7 6 5 4 3 2 1
每次划分都是左边有一个,右边 n - 1
T(n) = T(n - 1)+ O(n)
这里求得时间复杂度得用数学方法
T(n) = T(n - 1) + O(n)
T(n - 1) = T(n - 2) + O(n)
…
T(2) = T(1) + O(n)
那么显然,时间复杂度为 O(n^2)
最好情况就是,每次划分都在中间,每次都分为两段一样长的
T(n) = 2T(n / 2) + O(n)
a = 2, b = 2,k =1 --> a = b^k 由主定理法
时间复杂度为 O(nlogn)