STL源码剖析阅读笔记--线性容器(1)

好久好久么写了,看的书不少却很难落实记录下来,难得浮生半日闲,实验室项目暂告一段,最近阿里和中行面试终于基本完了,终于可以再捧起侯捷大神的STL,Mark一下,督促自己多书多记录,希望2019自己能在项目,毕设和找工作之间游刃有余。

目录

语法两则:

语法:

        1、临时对象的产生—一种无名对象,不在程序预期之下(passer by value会产生临时对象,负担)

2、静态常量整数成员在class内部可直接初始化

线性容器:

空间配置器

Vector

list

deque:

stack:

queue

heap

priority_queue和functional 的oush_heap pop_heap

push_heap和priority_queue的基本操作代码

两个堆的应用示例:中位数和合并k个链表

       中位数(牛客)

       合并K个链表(力扣23)


语法两则:

语法:

1、临时对象的产生—一种无名对象,不在程序预期之下(passer by value会产生临时对象,负担)

有时刻意制造临时对象,程序更加清楚----在类型后面加()且可指定初值,int(8),shape(3,5)

仿函数中的应用:

Template<typename T>

Class print

{

       public:void operator()(const T&element) const

{

       Cout<<element<<””;

}

}

for_each(iv.begin(),iv.end(),print<int>());    //请求的临时对象

2、静态常量整数成员在class内部可直接初始化

Class{

       Static const int a = 10;

       Static const long b = 20;

       Static const char c = ‘c’;

}

线性容器:

空间配置器

SGI标准空间配置器 std::allocator 仅仅是对new和delete的包装而已;效率低,不用。

SGI STL 每个容器的缺省配置为alloc

new ->operator new配置内存->构造函数

delete->调用析构函数->delete释放内存

 

内存配置器:

向heap申请空间,

考虑多线程状态,

考虑内存不足的应变措施,

考虑过多的小型区块可能造成的内存碎片问题

为了解决内存碎片问题alloc祭出双层配置器:

一级配置器:allocateà调用mallocà调用不成功调用oom_malloc

                     Realloc->调用realloc->调用不成功调用oom_realloc

二级配置器:(好像有点类似于linux内存分配的伙伴系统)

小额区块导致内存碎片,且配置时需要额外负担(需要一部分内存交给系统进行管理,交税,内存越小越浪费)。小于128字节则以内存池管理,维护与之对应的自由链表(free_list)

。分配内存则从freelist中找,释放小额内存则回收。

内存池维护16个free_list 8 16 24……128字节,为了方便管理内存,将内存碎片调整到8的倍数。内存节点使用共用体,当内存没有分配时,节点填充有指向节点的指针,维持其在链表中的位置,当节点分配后,填充客户数据,降低指针对内存的浪费。

allocate:

分配空间大于128字节则调用一级配置器,否则检查对应的free_list,如果有则拿来用,没有则调用refill为free_list蓄水。

 

refill chunk_alloc 内存池:

当free_list没有内存时,refill在内存池取多个(默认20)的相应内存给链表,其底层调用chunk_alloc函数,chunk_alloc以end_free – start_free判断内存池水位,水量充足则调出20个内存单位给free_list,其中一个给用户,19个给free_list维护。如果不足,但够一个内存单元则分配数目以引用的参数返回,如果内存池不够一个内存单元则malloc从heap中配置,引入新的活水,新水量为需求量的2倍(40)+附加量(正相关配置次数)。

deallocate():

       该函数先判断区块是否大于128字节,是则交给一级配置器去回收,否则搜索对应的free_list进行回收。

 

Vector

Vector的迭代器就是普通的指针。

空间配置器:alloc 动态内存分配,扩容会以原来两倍的空间拷贝赋值,原空间释放,。连续空间,任何引起内存重新配置操作到会导致迭代器失效。

 

erase pop_back等删除函数只能改变size不能改变capacity 根据源代码很容易推断出vector的capacity只增不减。

clear ->调用erase也只是改变size

Insert若空间不足会2倍扩容,迭代器失效。

list

底层是双向环状链表,一个指针即可完成所有操作,如下图,刻意增加一个空节点(尾后节点),便于头尾操作,其迭代器是封装节点类型的指针,重载* -> ++ --等操作符。配置器使用alloc作为配置器,封装了一个list_node_allocator便于节点的配置。由于链表的数据结构特性,除了操作的迭代器,其他迭代器不会失效。

 

List的成员函数操作:

Insert->返回迭代器在插入哨兵元素迭代器之前。

       push_back push_front调用insert

erase->删除哨兵节点之后的节点

       pop_front pop_back调用erase   remove进行循环调用erase;

clear->以尾后节点开始,尾后节点结束进行循环删除;最后置node为空节点。

Protected方法:transfer(position,first,last)将同类(一)链表的某一段移到链表的某个位置前面。

List.splice(iter,list2) 将list2拼接到iter之前à调用内部方法transfer

List.merge(list2)  合并有序链表->在某一链表结束后transfer剩余节点

Reverse()->调用transfer

Sort算法,由于STL算法里的sort必须支持Random Access Iterator所以只能用成员函数list.sort();

deque:

允许双端插入,常数时间,vector头插效率奇差(大量移动);dequeue没有容量概念,提供随机访问,其迭代器设计比较复杂,运算效率差,如对于sort算法先拷贝到vector在sort在拷贝回来。能不用尽量少用。

 

Deque的数据结构:一个中控器存储指针,各个指针指向各个buffer(默认512字节)。即一个个连续的小空间被中控器连接起,相比于vector扩容避免了复制等操作高效,但是迭代器很复杂,内存维护也复杂。

迭代器:迭代器需要维护关于当前buffer的指针,first,last,cur和所在buffer在中控器中的位置node。

++操作时,3判定cur==last,否则cur++,否则,set_node下一个节点,cur指向下一个缓冲头位置。--操作类似。

push_back和push_front:

map会维护start和finish两个迭代器,表示node的范围,create_map_and_node进行deque结构安排时,会根据元素个数和缓冲区大小分配node个数,num_nodes num_elements/buffer_size() + 1,多分配一个node。map的start和finish也会放在map的中间,便于两边的插入。

当缓冲区不够时进行map扩容,map扩容不需要复制拷贝,在原来的基础上进行扩充,需要复杂的迭代器操作。

 

成员函数:

pop_back():最后缓冲区有元素则cur指针前移,析构后面。没有则释放最后一个缓冲区,调整finish状态到前一个缓冲区最后一个位置,析构该元素。

pop_front():类似。

clear():将缓冲区析构释放掉,留一个缓冲区(初始状态)。

erase(position): 首先判断当前位置,if(index > size>>1)若在缓冲区前半段,将前面元素进行移位操作,将最前面冗余的元素去掉。否则后面的元素移动,去除最后的冗余元素。

Insert(position,value):在position之前插入value。若position.cur == start.cur即最前端则调用push_front,若在最后端则调用push_back。否则交给insert_aux操作,关于insert_aux也是首先判断index>size()/2,若前面元素少,则在最前端加入一个与第一元素相同的元素,然后向前逐个移动,否则在最后加一个,向后逐个移动。

stack:

适配器:默认以deque为底层数据结构,双向端口封闭头端,不能遍历,没有迭代器。

       Stack<int,list<int>>istack;  以list为底层容器,双向端口封闭头端。一般不用vector,扩容效率太差。

queue

适配器:默认以deque为底层数据结构,不能遍历,没有迭代器。

Queue<int,list<int>>iqueue;以list为底层容器。一般不用vector,扩容效率差。

heap

二叉堆,完全二叉树,默认为大顶堆。以连续空间表述,父子关系为i/2 2i 2i+1;所以底层一般用vector进行实现(可动态改变大小且连续空间)。

算法里面有make_heap push_heap pop_heap sort_heap 等STL组件有priority_queue实现,入队以任何顺序进入,出队以权值大小出队。Heap不提供迭代器,不支持遍历。

push_heap(first,last)算法:末端加空节点,以50比较,向上浮动,浮不动填入即可。被调用时,新元素必须在底层vector的尾端,所以v.push_back() + push_heap(begin,end)。

pop_heap算法:堆顶置空,用68和子节点较大的比较,将空节点进行下沉,沉到底为止。

调用时弹出原素在底层vector的尾端,并未删除,所以pop_heap(begin,end)+v.pop_back()。

 

priority_queue和functional 的oush_heap pop_heap

make_heap(first,last):将一段现有的数据转化为heap顺序。

sort_heap(first,last):将一个堆的数据进行排序(持续调用调用pop_heap将最大的放到vector最后面,弹完所有元素则vector已经排好序),破坏堆的特性。

priority_queue:优先级队列,默认Maxheap 底层封装vector+push_heap+pop_heap算法,适配器,没有迭代器,不能遍历。

priority_queue<int>ipq(first,last);ipq.top();ipq.pop();ipq.push(val);

push_heap和priority_queue的基本操作代码

#include"iostream"
#include"queue"
#include"functional"

using namespace std;


//使用方法 priority(Type,Conteiner,Function)
//std::priority_queue<T, std::vector<T>, greater<T>> pq;
//不加后面两个参数的话默认为 container 默认为vector function默认为less  大顶堆 
//小顶堆 基本类型 priority_queue<int, vector<int>, greater<int> >q3;
//自定义型 则重载< 
//函数greater在<functional>
class Teacher_08
{
public:
	Teacher_08()
	{
		age = 0;
		strcpy(name, "NULL");
	}
	Teacher_08(int age,char*pname)
	{
		this->age = age;
		strcpy(name,pname);
	}
	bool operator<(const Teacher_08 &a) const
	{
		return (this->age < a.age);
	}

	~Teacher_08()
	{}
	void print()
	{
		cout << name << '\t' << age << endl;
	}

public:
	char name[64];
	int age;
};






void baseOpt_priqueue()
{
	priority_queue<int> q1;
	//默认是最大值优先队列 q1等价于q2
	priority_queue<int, vector<int>, less<int>>q2;
	priority_queue<int, vector<int>, greater<int> >q3;
	q2.push(1);
	q2.push(3);
	q2.push(4);
	q2.push(6);
	cout << "q2.size" << q2.size() << endl;
	while (!q2.empty())
	{
		cout << q2.top() << ' ';
		q2.pop();
	}
	cout << endl;
	q3.push(6);
	q3.push(3);
	q3.push(2);
	q3.push(1);
	while (!q3.empty())
	{
		cout << q3.top() << ' ';
		q3.pop();
	}
	cout << endl;


	Teacher_08 v[]= { {39,"zhangsan"},{20,"lisi"},{28,"wangwu"} };
	priority_queue<Teacher_08>q4(v,v+3);

	while (!q4.empty())
	{
		Teacher_08 temp = q4.top();
		temp.print();
		q4.pop();
	}
	cout << endl;
}

//stl  heap算法
void heap_opt()
{
	//include<algorithm> "functional"
	int myints[] = { 10,20,30,5,15 };
	vector<int> v(myints, myints + 5);
	vector<int>::iterator it;

	//将[begin, end)范围进行堆排序,默认使用less<int>, 即最大元素放在第一个。
	make_heap(v.begin(), v.end());
	cout << "初始堆堆顶 : " << v.front() << endl;

	//将begin移动到end的前部,同时将剩下的元素堆排序为一个新的heap
	//配合vector的pop_back进行出堆操作
	pop_heap(v.begin(), v.end()); v.pop_back();//pop_heap将弹出的元素放在vector最后面,vector在弹出
	cout << "max heap after pop : " << v.front() << endl;

	//刚插入的(尾部)元素做堆排序
	v.push_back(100); push_heap(v.begin(), v.end());//vector先插入在push_heap进行调整
	cout << "max heap after push: " << v.front() << endl;

	//将一个堆做排序,最终成为一个有序的系列,可以看到sort_heap时,必须先是一个堆
	//(两个特性:1、最大元素在第一个 2、添加或者删除元素以对数时间),因此必须先做一次make_heap.
	sort_heap(v.begin(), v.end());//破坏堆的堆属性进行排序

	cout << "最终堆顺序:";
	for (unsigned i = 0; i<v.size(); i++) cout << " " << v[i];
	cout << endl;
}


void main()
{
	baseOpt_priqueue();
	heap_opt();
	system("pause");

}

运行结果:

两个堆的应用示例:中位数和合并k个链表

中位数(牛客)

题目描述:

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

思路:维护两个堆,大顶堆和小顶堆,使得两个堆的大小相同;

代码:

class Solution {
public:
	void Insert(int num)
	{
		if (((min.size() + max.size()) & 1) == 0)//目前总共偶数个数字
		{
			if (max.size()>0 && num<max[0])//偶数个应该放在最小堆,但是数字比大顶堆最大值小
			{
				//vector.push_back  push_back() 数字进堆
				//调整,先进大顶堆,将大顶堆最大放入最小堆保持元素差1
				max.push_back(num);
				//将front移动到end的前部,同时将剩下的元素重新构造成(堆排序)一个新的heap
				push_heap(max.begin(), max.end(), less<int>());

				num = max[0];

				//pop_heap  vector.pop_back数字出堆
				//将[first, last)范围进行堆排序,默认使用less<int>, 即最大元素放在第一个。
				pop_heap(max.begin(), max.end(), less<int>());
				max.pop_back();
			}
			min.push_back(num);
			push_heap(min.begin(), min.end(), greater<int>());
		}
		else
		{
			if (min.size()>0 && min[0]<num)//本来要插入大顶堆,结果num小顶堆最小值小
			{
				min.push_back(num);
				push_heap(min.begin(), min.end(), greater<int>());

				num = min[0];

				pop_heap(min.begin(), min.end(), greater<int>());
				min.pop_back();
			}
			max.push_back(num);
			push_heap(max.begin(), max.end(), less<int>());
		}
	}

	double GetMedian()
	{
		int size = min.size() + max.size();
		double median = 0;
		if (size & 1 == 1)
			median = min[0];
		else
			median = (min[0] + max[0]) / 2.0;//必须写2.0否则会舍弃不对
		return median;
	}
private:
	vector<int>min;//小顶堆  后半部分  保持大小堆差一个,偶数个新来的放最小堆即后面
	vector<int>max;//大顶堆  前半部分  奇数个新来的放最大堆
};

 

运行结果:

 

合并K个链表(力扣23)

题目描述:

合并 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例:

输入:
[
  1->4->5,
  1->3->4,
  2->]

思路:维护一个小顶堆,大小为k,现将k个链表的头节点入堆,进行出堆操作,堆顶出堆连接到新链表尾部,直到堆为空;

代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
 struct greater_node
 {
	 bool operator()(ListNode*a, ListNode*b)
	 {
		 return (a->val > b->val);
	 }
 };

class Solution {
public:
	ListNode* mergeKLists(vector<ListNode*>& lists) {
		priority_queue<ListNode*,vector<ListNode*>,greater_node> pq;
		ListNode*ret = nullptr;
		ListNode*cur = nullptr;

		for (auto node : lists)
		{
			if (node != nullptr)
				pq.push(node);
		}
		while (!pq.empty())
		{
			ListNode*node = pq.top();
			pq.pop();
			if (nullptr == ret)
			{
				ret = node;
				cur = node;
			}
			else
			{
				cur->next = node;
				cur = cur->next;
			}
			if (nullptr != node->next) {
				pq.push(node->next);
			}
		}
		return ret;
	}
};

运行结果:

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值