算法与数据结构应用

数组越界

在C语言中,只要不是受限内存,都可以直接访问。当越界的数组a[index]的时候,会被定位到可能不属于数组的内存上。如果这个内存可用,那么不会报任何错误,这就会导致逻辑错误。一些语言如Java会做越界检查。

数组下标

数组是通过一组连续的内存空间来存储的。当需要访问其中一个数组元素的时候,会通过计算内存来访问

a[i]_address = base_address + i * data_type_size

data_type_size代表数组中每个元素的大小,比如int是4个字节。所以数组下标其实表示的是偏移量i的值。

容器类能否完全代替数组

数组本身在定义的时候需要预先指定大小,因为需要分配连续的内存空间。如果使用ArrayList,我们不需要关心底层的扩容逻辑,每次存储空间不够的时候,它将会自动扩容成1.5倍大小。因为扩容需要申请内存和数据搬移,是比较耗时的,最好在创建ArrayList的时候,先指定数据大小。
如果我们用 ArrayList 存储了了 1GB 大小的数据,这个时候已经没有空闲空间了,
当我们再插入数据的时候,ArrayList 会申请一个 1.5GB 大小的存储空间,并且把原来那 1GB 的数据拷贝到新申请的空间上。

Java ArrayList无法存储基本类型,比如 int、long,需要封装为 Integer、Long类。而 Autoboxing,Unboxing 则有一定的性能消耗。

链表实现LRU

缓存是一种提高读取数据性能的方式,在硬件设计和软件开发中都有广泛的应用,比如常见的CPU缓存,数据库缓存和浏览器缓存等。缓存的清除有一种策略叫做最近最少使用策略LRU。
链表和数组不同,不一定使用连续的内存。链表通过指针来将一组零散的内存块串起来,我们把内存块称为链表的结点,结点除了存储数据之外,还要存储下一个结点的指针next。
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。双向链表在删除给定指针指向的结点时比单链表要高效,因为单链表只能通过遍历才能知道上一个结点是谁,而双向链表并不需要。Java中的LinkedHashMap容器底层就是通过双向链表实现的。
缓存的核心就是空间换时间。对于执行较慢的程序,可以通过消耗更多的内存来进行优化。那么我们实现一个简单的LRU就可以通过一个单链表来实现,我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。
如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
如果此数据没有在缓存链表中,又可以分为两种情况:如果此时缓存未满,则将此结点直接插入到链表的头部;如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
有些语言有“指针”的概念,比如 C 语言;有些语言没有指针,取而代之的是“引用”。实际上,它们的意思都是一样的,都是存储所指对象的内存地址。
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
如果我们要删除链表中的最后一个结点

if (head->next == null) {
   head = null;
}

如果我们引入哨兵结点,在任何时候,不管链表是不是空,head 指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表。

栈在表达式求值中的应用

如3+5*8-6 这个表达式的计算过程,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。
如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。

猜你喜欢

转载自blog.csdn.net/sysuzhyupeng/article/details/83240356