大一计算机的自学总结:堆结构和堆排序

前言

堆本质上是一种树,也是一种重要的数据结构。堆排序的时间复杂度和归并排序随机快排一样,都是O(n*logn)。

一、堆结构

堆其实是一种完全二叉树,完全二叉树就是若按层序遍历整棵树并将每个节点编号,到最后编号是连续的。

由定义可知,若将数组的下标看作节点编号,任何数组都可以表示为一个完全二叉树。所以,通过将数组看作一个堆结构,就可以实现用堆排序一个数组。

1.大根堆

大根堆就是在堆结构这个完全二叉树上,任何一颗子树都满足最大值在顶部。以下是将数组建成大根堆的过程。

其中用到的公式有:父节点=(当前节点-1)/2,左孩子=当前节点*2+1,右孩子=当前节点*2+2

(1)向上插入
//向上插入 
void heapInsert(int i)
{
	while(arr[i]>arr[(i-1)/2])
	{
		swap(i,(i-1)/2);
		i=(i-1)/2;	
	}	
} 

首先是向上插入,因为大根堆的特点就是最大值在顶部,所以只要当前节点的值比父节点的值还大,就要交换,然后让i来到交换后原来节点的父节点的位置。

可以理解为,向上插入就是孩子不断向上干他父亲的过程。(孝)

(2)向下调整
//向下调整
void heapify(int i,int size)
{
	int l=2*i+1;
	while(larr[l]?l+1:l;
		best=arr[best]>arr[i]?best:i;
		if(best==i)
		{
			break;	
		}	
		swap(best,i);
		i=best;
		l=2*i+1;
	}	
} 

当某个节点的值发生改变(这里默认减小),肯定要向下和他的孩子进行调整,所以先让变量l来到当前节点左孩子的位置,然后只要l没越界,即存在左孩子,那就设置best变量表示当前节点左孩子和右孩子中值较大的孩子位置,注意还要保证存在右孩子。之后改变best让其等于当前节点和较大孩子中较大数的位置,若best=i,即当前节点比他的左右孩子都大,就说明调整结束,跳出循环;若没有就交换,让i来到best的位置,l继续等于i的左孩子。

可以理解为,向下调整就是父亲没实力不断降辈分的过程。(乐)

2.小根堆

与大根堆相反,小根堆就是最小值在顶部。

二、堆排序

借助大根堆最大值在顶部的特性,就可以完成对数组的排序。

#include
using namespace std;

//全局变量
#define n 10
int arr[n]; 

//交换
void swap(int a,int b)
{
	int t=arr[a];
	arr[a]=arr[b];
	arr[b]=t;	
} 

//向下调整
void heapify(int i,int size)
{
	int l=2*i+1;
	while(larr[l]?l+1:l;
		best=arr[best]>arr[i]?best:i;
		if(best==i)
		{
			break;	
		}	
		swap(best,i);
		i=best;
		l=2*i+1;
	}	
} 

//向上插入 
void heapInsert(int i)
{
	while(arr[i]>arr[(i-1)/2])
	{
		swap(i,(i-1)/2);
		i=(i-1)/2;	
	}	
} 

//堆排序
void heapSort(void)
{
	for(int i=0;i1)
	{
		swap(0,--size);
		heapify(0,size);
	}
}

int main()
{
	for(int i=0;i>arr[i];
	}
	
	//堆排序 
	heapSort();
	
	cout<<"Sorted:"<

首先从顶到底建大根堆,然后设置size表示此时数组中未排序部分的长度,因为大根堆的特性,数组中第一个数必定是最大值,就可以将其交换到size-1的位置。此时堆结构中顶部的值减小,所以要进行向下调整的步骤。最后,当size=1时,整个数组已经有序。

三、从底到顶建堆

观察发现,由于二叉树顶部的节点个数必然少于底部的节点个数,而从顶到底的建堆方法中,顶部经历的向上插入过程数又少于底部,所以必然会繁琐一点。而从底到顶的建堆方法中,由于是从底部开始,所以底部经历的向下调整过程数会少于顶部,所以会简便很多。

#include
using namespace std;

//全局变量
#define n 10
int arr[n]; 

//交换
void swap(int a,int b)
{
	int t=arr[a];
	arr[a]=arr[b];
	arr[b]=t;	
} 

//向下调整
void heapify(int i,int size)
{
	int l=2*i+1;
	while(larr[l]?l+1:l;
		best=arr[best]>arr[i]?best:i;
		if(best==i)
		{
			break;	
		}	
		swap(best,i);
		i=best;
		l=2*i+1;
	}	
} 

//堆排序
void heapSort(void)
{
	for(int i=n-1;i>=0;i--)
	{
		heapify(i,n);
	}
	int size=n;
	while(size>1)
	{
		swap(0,--size);
		heapify(0,size);
	}
}

int main()
{
	for(int i=0;i>arr[i];
	}
	
	//堆排序 
	heapSort();
	
	cout<<"Sorted:"<

只需要将从0开始遍历数组向上插入的步骤改为从n-1开始向下调整的步骤即可。

 四、堆结构例题

1.合并k个已排序的链表

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
#include 
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param lists ListNode类vector 
     * @return ListNode类
     */
    static bool cmp(ListNode* &a,ListNode* &b)
    {
        return a->val>b->val;
    }

    ListNode* solve(vectorlists)
    {
        priority_queue,decltype(&cmp)>heap(cmp);
        for(int i=0;inext!=NULL)
            {
                heap.push(cur->next);
            }
            tail->next=heap.top();
            heap.pop();
            tail=tail->next;
            cur=tail;
        }

        return head;
    }

    ListNode* mergeKLists(vector& lists) {
        // write code here
        return solve(lists);
    }
};

因为要按顺序合并多个有序链表,可以借助小根堆的特性,即最小值在顶部。

因为priority_queue中存放的是节点地址,而又要根据节点数组进行比较,所以借助decltype比较器和cmp比较函数生成小根堆。注意decltype的用法中,若cmp函数返回值为true,则说明b的优先级大于a

之后先就将每个头节点入堆,之后每次连接小根堆的堆顶,并将该节点的next节点入堆即可。注意排除指针为NULL的头节点。

2.线段重合

#include 
using namespace std;

static bool cmp(pair&a,pair&b)
{
    return a.first>n;

    pairinterval;
    vector >line;
    int a,b;
    for(int i=0;i>a>>b;
        line.push_back({a,b});
    }

    sort(line.begin(),line.end(),cmp);

    priority_queue,greater >heap;
    int ans=0;
    for(int i=0;i

这个问题的思路就比较抽象了,思考重合线段的特点,注意到,重合线段的左边界必然是某一条线段的左边界。(注意力惊人)

之后借助小根堆的特性,先根据每条线段的左边界排序,之后遍历所有线段,当堆不为空且堆顶元素小于等于线段的左端点时,弹出堆顶。然后压入线段右端点。最后让ans等于堆大小和自身的较大值即可。

原理大致可以理解为,堆顶是某条线段右边界的最小值,若该数值小于等于新线段的左边界,则说明堆顶线段已经不包括在重合线段的范围内,就弹出,然后入堆新线段的右边界。最后堆大小的最大值即为最多重合线段数。

3.将数组和减半的最少操作次数

class Solution {
public:
    int halveArray(vector& nums) {
        priority_queueheap;
        double sum=0;
        for(int i=0;i

这个问题相比上一个就简单很多了,将数组和减半,又要最少次数,所以每次都减半数组中最大值即可。其中数组元素的最大值就可以借助大根堆。

注意在减半操作中会出现小数,要使用double类型。

总结

堆结构在一些重点关注最大值最小值的问题中用处还是很大的。

END

你可能感兴趣的:(c++,数据结构,排序算法,leetcode)