由于一些主观方面的原因,我大一都是面向应用、面向爱好编程,闯闯荡荡发现做的事情都没有什么太大的技术含量,懂得了很多东西固然是一件好事,但是算法能力的欠缺让我现在举步维艰,痛定思痛从大二上考完定制了一套属于自己的算法学习规划,这个系列我将以学习过程中的一些有趣的知识点以及自己的思考点来阐述,并会附带部分leetcode上刷的算法题解
问题一:为什么大多数编程语言中,数组要从0开始编号,而不是1?
数组的内存模型,准确来说下标最确切说是偏移量,如果用a来表示数组的首地址,a[0]就是偏移为0的位置,也就是首地址,而数组在随机访问下标为k的元素时,会通过首地址以及偏移量来计算该元素所在的地址来取值,a[k]就表示偏移k个type_size的位置,公式为:a[k]_address = base_address + k * type_size,而如果使用1来编号,那么求址公式就会变为a[k]_address = base_address + (k-1)*type_size,这样在随机访问数组元素时多了一个减法运算,对于CPU来说,就是多了一次减法指令。
数组VS链表
1.数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。
2.数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足“。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,这也是它与数组最大的区别。
综上,如果你的代码对内存的使用非常苛刻,那数组就更适合你。因为链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。而且,对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片,如果是Java语言,就有可能会导致频繁的GC(Garbage Collection,垃圾回收)。
CPU缓存机制:
CPU在从内存读取数据的时候,会先把读取到的数据加载到CPU的缓存中。而CPU每次从内存读取数据并不是只读取那个特定要访问的地址,而是读取一个数据块(这个大小我不太确定。。)并保存到CPU缓存中,然后下次访问内存数据的时候就会先从CPU缓存开始查找,如果找到就不需要再从内存中取。这样就实现了比内存访问速度更快的机制,也就是CPU缓存存在的意义:为了弥补内存访问速度过慢与CPU执行速度快之间的差异而引入。对于数组来说,存储空间是连续的,所以在加载某个下标的时候可以把以后的几个下标元素也加载到CPU缓存这样执行速度会快于存储空间不连续的链表存储。
基于链表的LRU缓存淘汰算法
1)什么是缓存?
缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非广泛的应用,比如常见的CPU缓存、数据库缓存、浏览器缓存等等。
2)为什么使用缓存?即缓存的特点
缓存的大小是有限的,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?就需要用到缓存淘汰策略。
3)什么是缓存淘汰策略?
指的是当缓存被用满时清理数据的优先顺序。
4)有哪些缓存淘汰策略?
常见的3种包括先进先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frenquently Used)、最近最少使用策略LRU(Least Recently Used)。
5)链表实现LRU缓存淘汰策略
1.如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
2.如果此数据没有在缓存链表中,又可以分为两种情况:
如果此时缓存未满,则将此结点直接插入到链表的头部;
如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
leetcode
1.盛水最多的容器
方法一:暴力求解法
class Solution {
public int maxArea(int[] height) {
int maxarea = 0;
for (int i = 0; i < height.length; i++)
for (int j = i + 1; j < height.length; j++)
maxarea = Math.max(maxarea, Math.min(height[i], height[j]) * (j - i));
return maxarea;
}
方法二:双指针法
我们在由线段长度构成的数组中使用两个指针,一个放在开始,一个置于末尾。 此外,我们会使用变量 maxareamaxarea 来持续存储到目前为止所获得的最大面积。 在每一步中,我们会找出指针所指向的两条线段形成的区域,更新 maxareamaxarea,并将指向较短线段的指针向较长线段那端移动一步。
public class Solution {
public int maxArea(int[] height) {
int maxarea = 0, l = 0, r = height.length - 1;
while (l < r) {
maxarea = Math.max(maxarea, Math.min(height[l], height[r]) * (r - l));
if (height[l] < height[r])
l++;
else
r--;
}
return maxarea;
}
}
这个算法可以这样考虑,无论如何容量总是由短板和底部的长度来决定,我们通过两个指针从外向内收可以遍历所有的状态,无论移动长板或者短板,底部长度总是减少,现在比较移动完后的短板,而若是让长板往里面收一格,那么短板的长度只会减小或者不变,所有一定不会出现最大状态,移动短板往内收一格,短板可能增大,才有可能出现较大状态。
2.最接近的三数之和
解法一,暴力求解,三个for循环,时间复杂度为O(n^3),不再阐述解法
解法二,双指针法,先排序,然后一个外层for循环,然后用双指针,一个指向左边,一个指向右边,这样可以根据现有的三个和来调整指针移动来接近target,记录下每次的和,求解Min(Math.abs(sum - target))
class Solution {
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int ans = nums[0] + nums[1] + nums[2];
for(int i=0;i<nums.length;i++) {
int start = i+1, end = nums.length - 1;
while(start < end) {
int sum = nums[start] + nums[end] + nums[i];
if(Math.abs(target - sum) < Math.abs(target - ans))
ans = sum;
if(sum > target)
end--;
else if(sum < target)
start++;
else
return ans;
}
}
return ans;
}
}