入门基础2
1.荷兰国旗问题(用的是快排思想)
提法1:给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。
要求额外空间复杂度O(1),时间复杂度O(N)
提法2:给定一个数组arr,和一个数num,请把小于num的数放在数组的:左边,等于num的数放在数组的中间,大于num的数放在数组的右边。
要求额外空间复杂度O(1),时间复杂度O(N)
Demo:
public class NetherLandFlag {
public static void partition(int[] arr,int left,int right,int num){
int less = left - 1;//小区域,存放小于目标数的区域
int more = right + 1;//大区域,存放大于目标数的区域
int index_cur = left;//从数组的左边开始遍历
while(index_cur < more){//当索引没有走到右边的时候就循环
if(arr[index_cur] < num){//当索引值小于给定数字
swap(arr,++less,index_cur++);
}else if(arr[index_cur] > num){//当索引值大于给定的数
swap(arr,--more,index_cur);
}else {//当索引值等于给定的数
index_cur++;
}
}
}
public static void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {2,3,1,9,7,6,1,4,5};
int num = 4;
System.out.println("原数组为:"+ Arrays.toString(arr));//[2, 3, 1, 9, 7, 6, 1, 4, 5]
partition(arr,0,arr.length - 1,num);
System.out.println("荷兰国旗排法:"+Arrays.toString(arr));//[2, 3, 1, 1, 4, 6, 7, 5, 9]
}
}
一个简单图解:
2.快速排序
先来个使用split方法简单的图解(这只是快排的第一步,步骤都是类似,都是拿第一个元素作为基准的):
此方法的效率还是不够高,下边使用partition方法:
借鉴快速排序(java实现)
- 方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。
方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:
6 1 2 5 9 3 4 7 10 8
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:
6 1 2 5 4 3 9 7 10 8
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:
3 1 2 5 4 6 9 7 10 8
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。
这是传统的以左边为基准的做法,代码demo:
public class QuickSort{
public static void quickSort2(int[] arr){
if(arr == null || arr.length < 2){
return;
}
partition_left(arr,0,arr.length - 1);
}
public static void partition_left(int[] arr,int l,int r){//以左边为基准的分割排序方法,这是传统的做法
if(l > r){
return;
}
int low = l;
int high = r;
while(low < high){
while (arr[high] >= arr[l] && low < high )//必须先从右边开始,寻找第一个小于基准数的数字
high--;
while (arr[low] <= arr[l] &&low < high)//再从左边开始搜索,寻找第一个大于基准数的数字
low++;
if(low < high)
swap(arr,low,high);
}
swap(arr,l,low);//左边的基准数与当前指向的数交换
partition_left(arr,l,low-1);
partition_left(arr,low + 1,r);
}
public static void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {2,3,1,9,7,6,1,4,5};
quickSort2(arr);
System.out.println(Arrays.toString(arr));//[1, 1, 2, 3, 4, 5, 6, 7, 9]
}
}
还有以右边为基准的,类似于上边的荷兰国旗的demo做法,这时候不会浪费已经比较过的相等资源:
public class QuickSort_NetherLandFlag {
public static void quickSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
quickSort(arr,0,arr.length - 1);
}
public static void quickSort(int[] arr,int l,int r){
if( l < r){
int[] p = partition(arr,l,r);
quickSort(arr,l,p[0] - 1);
quickSort(arr,p[1]+1,r);
}
}
public static int[] partition(int[] arr,int l,int r){//荷兰国旗的做法,这里先以右边的数作为基准
int less = l - 1;
int more = r;
while(l < more){
if(arr[l] < arr[r]){//右边为基准,总是比较右边的
swap(arr,++less,l++);
}else if(arr[l] > arr[r]){
swap(arr,--more,l);
} else {
l++;
}
}
swap(arr,more,r);//这时候的r和l是相等的了
return new int[]{less + 1,more};
}
public static void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {2,3,1,9,7,6,1,4,5};
quickSort(arr);
System.out.println(Arrays.toString(arr));//[1, 1, 2, 3, 4, 5, 6, 7, 9]
}
}
这些都是经典快排,时间复杂度都与数据的状况相关,最坏的情况下复杂度为O( ),最好的情况就是O(N),平均情况为O(N*logN),而随机快排的时间复杂度是一个期望表达式,时间复杂度可以当做是O(N*logN)。它的实现就是在上边的真正排序前,将基准随机交换一下:
public static void quickSort(int[] arr,int l,int r){
if( l < r){
swap(arr,l + (int)Math.random()*(r - l + 1),r);//加多这句就是随机快排
int[] p = partition(arr,l,r);
quickSort(arr,l,p[0] - 1);
quickSort(arr,p[1]+1,r);
}
}
随机快排是三个经典的O(N*logN)算法之一,也是最常用的排序算法!,代码简洁,也意味着,常数项将会很小,耗费小。快排的额外空间复杂度是O(logN)。
3.堆排序
堆结构其实是一个完全二叉树。
3.1树结构
1)树结构
树是n个结点的有限集合,有且仅有一个根结点,其余结点可分为m个根结点的子树。
2)树的概念
- 结点的度:一个结点拥有子树的个数称为度。比如A的度为3,C的度为2,H的度为0。度为0的结点称为叶子节点(D,F,G,H)。树的度是树中所有结点的度的最大值,此树的度为3。
- 树中结点的最大层次成为树的深度或高度。此树的深度为4。
- 父节点A的子结点B,C,D;B,C,D也是兄弟节点
- 树的集合称为森林.树和森林之间有着密切的关系.删除一个树的根结点,其所有原来的子树都是树,构成森林.用一个结点连接到森林的所有树的根结点就构成树.
- 叶子结点和子结点:叶子结点后边不跟有任何的结点。子结点后边还会跟有结点。
3)二叉树
二叉树是每个节点最多拥有两个子节点,左子树和右子树是有顺序的不能任意颠倒。
4)满二叉树
高度为h,由2^h-1个节点构成的二叉树称为满二叉树。
一般来说,两个节点都有的都是满二叉树。因为定义:
除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
5)完全二叉树
完全二叉树是由满二叉树而引出来的,若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数(即1~h-1层为一个满二叉树),第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
堆一般都是用完全二叉树来实现的。
完全二叉树与非完全二叉树的区分:
满二叉树属于完全二叉树,但是完全二叉树不一定是满二叉树!
3.2 堆结构(大根堆和小根堆)
堆就是完全二叉树!
性质:每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。如下图:
对上面的图中每个数都进行了标记,上面的结构映射成数组就变成了下面这个样子:
还有一个基本概念:查找完全二叉树(其实就是数组)中某个数的父结点和左右孩子结点,比如已知索引为 i 的数,那么:
1.i的父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)
2.i的左孩子索引:2*i+1
3.i的右孩子索引:2*i+2
所以上面两个数组可以脑补成堆结构,因为他们满足堆的定义性质:
-
大根堆:arr(i)>arr(2i+1) && arr(i)>arr(2i+2)
-
小根堆:arr(i)<arr(2i+1) && arr(i)<arr(2i+2)
其实根本就没有什么完全二叉树这个结构,就是一个数组而已。别想得太高大上。数组逻辑上实现完全二叉树就是完全二叉树了。
脑补神图(和上边的小根堆一样的):
3.3堆排序的实现步骤
基本思想【重点】:
1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
步骤:借鉴堆排序算法(图解详细流程)
1. 构造堆
将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)
假设存在以下数组:
主要思路:第一次保证00位置大根堆结构(废话),第二次保证01位置大根堆结构,第三次保证02位置大根堆结构…直到保证0n-1位置大根堆结构(每次新插入的数据都与其父结点进行比较,如果插入的数比父结点大,则与父结点交换,否则一直向上交换,直到小于等于父结点,或者来到了顶端)。
插入6的时候,6大于他的父结点3,即arr(1)>arr(0),则交换;此时,保证了0~1位置是大根堆结构,如下图:
插入8的时候,8大于其父结点6,即arr(2)>arr(0),则交换;此时,保证了0~2位置是大根堆结构,如下图:
插入7的时候,7大于其父结点5,则交换,交换之后,7又发现比8小,所以不交换;此时整个数组已经是大根堆结构
2. 固定最大值再构造堆
此时,我们已经得到一个大根堆,下面将顶端的数与最后一位数交换,然后将剩余的数再构造成一个大根堆
此时最大数8已经来到末尾,则固定不动,后面只需要对顶端的数据进行操作即可,拿顶端的数与其左右孩子较大的数进行比较,**如果顶端的数大于其左右孩子较大的数,则停止,如果顶端的数小于其左右孩子较大的数,则交换,**然后继续与下面的孩子进行比较。
下图中,5的左右孩子中,左孩子7比右孩子6大,则5与7进行比较,发现5<7,则交换;交换后,发现5已经大于他的左孩子,说明剩余的数已经构成大根堆,后面就是重复固定最大值,然后构造大根堆:
如下图:顶端数7与末尾数3进行交换,固定好7,
剩余的数开始构造大根堆 ,然后顶端数与末尾数交换,固定最大值再构造大根堆,重复执行上面的操作,最终会得到有序数组:
3.上述步骤的代码实现
//堆排序
public static void heapSort(int[] arr) {
//构造大根堆
heapInsert(arr);
int size = arr.length;
while (size > 1) {
//固定最大值
swap(arr, 0, size - 1);
size--;
//构造大根堆
heapify(arr, 0, size);
}
}
//构造大根堆(通过新插入的数上升)
public static void heapInsert(int[] arr) {
for (int i = 0; i < arr.length; i++) {
//当前插入的索引
int currentIndex = i;
//父结点索引
int fatherIndex = (currentIndex - 1) / 2;
//如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点
//然后继续和上面的父结点值比较,直到不大于父结点,则退出循环
while (arr[currentIndex] > arr[fatherIndex]) {
//交换当前结点与父结点的值
swap(arr, currentIndex, fatherIndex);
//将当前索引指向父索引
currentIndex = fatherIndex;
//重新计算当前索引的父索引
fatherIndex = (currentIndex - 1) / 2;
}
}
}
//将剩余的数构造成大根堆(通过顶端的数下降)
public static void heapify(int[] arr, int index, int size) {
int left = 2 * index + 1;
int right = 2 * index + 2;
while (left < size) {
int largestIndex;
//判断孩子中较大的值的索引(要确保右孩子在size范围之内)
if (arr[left] < arr[right] && right < size) {
largestIndex = right;
} else {
largestIndex = left;
}
//比较父结点的值与孩子中较大的值,并确定最大值的索引
if (arr[index] > arr[largestIndex]) {
largestIndex = index;
}
//如果父结点索引是最大值的索引,那已经是大根堆了,则退出循环
if (index == largestIndex) {
break;
}
//父结点不是最大值,与孩子中较大的值交换
swap(arr, largestIndex, index);
//将索引指向孩子中较大的值的索引
index = largestIndex;
//重新计算交换之后的孩子的索引
left = 2 * index + 1;
right = 2 * index + 2;
}
}
//交换数组中两个元素的值
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
4.堆排序的另一种代码实现
public class HeapSort{
//堆排序流程就是:构建大根堆,将大顶堆的顶和底交换,然后剩下的n-1个数继续前边的操作(后边的操作更多是一种寻找堆中最大值的操作)
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);//构建大顶堆
}
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);//后边的n-1次构建大根堆
swap(arr, 0, --size);//每次都要交换最大、最小值
}
}
public static void heapInsert(int[] arr, int index) {//堆或者是完全二叉树,这种结构有个显著的特点就是,总是先从左边的数开始排满的
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;//不断地循环回去,直到顶的结点的时候,index为0,但是(index - 1) / 2也为0,所以循环到顶了就终止,构建堆或者完全二叉树这种结构的时候根本就不用考虑兄弟结点的,所以只考虑左结点就是对的
}
}
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;//找出大值的索引,比较的是兄弟节点
largest = arr[largest] > arr[index] ? largest : index;//找出大值的索引,与父节点比较
if (largest == index) {//如果最大值的索引已经等于父索引,就不再继续循环了
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
int[] arr = {2,1,4,3,9,5,6};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
}
5.复杂度介绍
在堆结构中,对于一个拥有N个结点的堆来说,他的高度(深度)为logN(以2为底),在此结构中插入一个数要付出的代价就为logN,就是说插入一个数,最多动logN这么多次就行了。建立大根堆的过程复杂度为:O(N)。堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序.
6.利用对结构实现的一些强大功能(贪心等)
1)求中位数(借鉴用最大堆和最小堆实现中位数查找):
具体思路:
用一个最大堆存放比中位数小(或等于)的元素,用一个最小堆存放比中位数大(或等于)的元素。这里关键的方法是insert(),每当要插入一个元素时,根据判断条件将它插入最大堆或是最小堆,并更新最大堆和最小堆,使得最大堆和最小堆中元素的个数之差不超过1,这样中位数就是最大堆或最小堆的堆顶元素。当最大堆和最小堆中元素个数不同(个数相差为1)时,元素个数多的那个堆的堆顶元素即为中位数;如果两者元素个数相同,那么中位数可以是最大堆和最小堆的堆顶元素的值取平均。下面的程序代码中,当两者元素个数相同时,将最大堆的堆顶元素看做中位数。
插入步骤(insert) :
(1)如果最大堆为空,将元素插入最大堆;
(2)如果最小堆为空,将元素插入最小堆;
(3)如果元素比最大堆的堆顶元素小且最大堆中元素个数不大于最小堆中元素个数,将元素插入最大堆;如果如果元素比最大堆的堆顶元素小但最大堆中元素个数大于最小堆中元素个数,那么先把最大堆的堆顶元素插入最小堆,然后删除最大堆的堆顶元素,最后把元素插入最大堆;
(4)如果元素比最小堆的堆顶元素大且最小堆中元素个数不大于最大堆中元素个数,将元素插入最小堆;如果如果元素比最小堆的堆顶元素大但最小堆中元素个数大于最大堆中元素个数,那么先把最小堆的堆顶元素插入最大堆,然后删除最小堆的堆顶元素,最后把元素插入最小堆;
(5)如果最大堆中元素个数小于最小堆中元素个数,将元素插入最大堆;否则将元素插入最大堆。
public class Midian<Key> {
private MaxPQ max; //store items less than midian
private MinPQ min; //store items larger than midian
/**
* Initialize max and min
*/
public Midian(){
max = new MaxPQ();
min = new MinPQ();
}
/**
* Is both max and min are empty?
*/
public boolean isEmpty(){
return max.isEmpty() && min.isEmpty();
}
/**
* Add a new key to either max or min.
*/
public void insert(Key k){
if(max.isEmpty()) {
max.insert(k);
return;
}
if(min.isEmpty()){
min.insert(k);
return;
}
if(less(k, (Key)max.max())) {
if(max.size() <= min.size()) max.insert(k);
else {
min.insert(max.delMax());
max.insert(k);
}
}
else if(less((Key)min.min(), k)){
if(min.size() <= max.size()) min.insert(k);
else{
max.insert(min.delMin());
min.insert(k);
}
}
else{
if(max.size() < min.size()) max.insert(k);
else min.insert(k);
}
}
/**
* Return the midian key on max or min.
* Throw an exception if both max and min empty.
*/
public Key midian(){
if (isEmpty()) throw new RuntimeException("Both max and min are underflow");
if(max.size() < min.size()) return (Key)min.min();
return (Key)max.max();
}
private boolean less(Key k1, Key k2){
return ((Comparable)k1).compareTo(k2) < 0;
}
/**
* A test client.
*/
public static void main(String[] args){
Midian midian = new Midian();
while(!StdIn.isEmpty()){
String item = StdIn.readString();
if(!item.equals("-")) midian.insert(item);
else if(!midian.isEmpty()) StdOut.print(midian.midian());
}
}
}