424.LeetCode

129 阅读3分钟

买卖股票的最佳时机

方法一:暴力法

var maxProfit = function(prices) {
	let maxprofit = 0;
	for(let i = 0;i < prices.length-1;i++){
		for(let j = i+1;j<prices.length;j++){
			let profit = prices[j]-prices[i];
			if(profit > maxprofit){
				maxprofit=profit;
			}
		}
	}
	return maxprofit
};

复杂度分析

时间复杂度:O(n2)O(n^2)。循环运行 n(n1)2\dfrac{n (n-1)}{2} 次。

空间复杂度:O(1)O(1)。只使用了常数个变量。

方法二:一次遍历

显然,如果我们真的在买卖股票,我们肯定会想:如果我是在历史最低点买的股票就好了!太好了,在题目中,我们只要用一个变量记录一个历史最低价格 minprice,我们就可以假设自己的股票是在那天买的。那么我们在第 i 天卖出股票能得到的利润就是 prices[i] - minprice。

因此,我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案。

var maxProfit = function(prices) {
	let minprice = 100;
	let maxprofit = 0;
	for(let i = 0;i < prices.length;i++){
		if(prices[i]<minprice){
			minprice= prices[i];
		}else if (prices[i]-minprice > maxprofit){
			maxprofit = prices[i]-minprice;
		}
	}
	return maxprofit;
};

时间复杂度:O(n)O(n),只需要遍历一次。

空间复杂度:O(1)O(1),只使用了常数个变量。

无重复字符的最长子串

滑动窗口

我们使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 rkr_k

在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;

在枚举结束后,我们找到的最长的子串的长度即为答案。

判断重复字符

在上面的流程中,我们还需要使用一种数据结构来判断 是否有重复的字符,常用的数据结构为哈希集合(即 C++ 中的 std::unordered_set,Java 中的 HashSet,Python 中的 set, JavaScript 中的 Set)。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。

var lengthOfLongestSubstring = function(s){
	// 哈希集合,记录每个字符是否出现过
	const occ = new Set();
	const n = s.length;
	let rk = -1,ans = 0;
	for(let i = 0;i < n;++i){
		if(i!=0){
			occ.delete(s.charAt(i-1));
		}
		while(rk+1<n&&!occ.has(s.charAt(rk+1))){
			occ.add(s.charAt(rk+1));
			++rk;
		}
		ans = Math.max(ans,rk-i+1);
	}
	return ans;
};

时间复杂度:O(N)O(N),其中 N是字符串的长度。左指针和右指针分别会遍历整个字符串一次。

空间复杂度:O(Σ)O(|\Sigma|),其中Σ \Sigma表示字符集(即字符串中可以出现的字符),Σ|\Sigma| 表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在 [0,128)[0, 128) 内的字符,即 Σ=128|\Sigma| = 128。我们需要用到哈希集合来存储出现过的字符,而字符最多有 Σ|\Sigma| 个,因此空间复杂度为 O(Σ)O(|\Sigma|)处。

LRU缓存机制

哈希表 + 双向链表

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。

哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1)O(1)的时间内完成 get 或者 put 操作。具体的方法如下:

对于 get 操作,首先判断 key 是否存在:

如果 key 不存在,则返回 −1;

如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。

对于 put 操作,首先判断 key 是否存在:

如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;

如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

上述各项操作中,访问哈希表的时间复杂度为 O(1)O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1)O(1) 时间内完成。

小贴士

在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。

class LRUCache extends LinkedHashMap<Integer,Integer>{
	private int capacity;

	public LRUCache(int capacity){
		super(capacity,0.75F,true);
		this.capacity = capacity;
	}

	public int get(int key){
		return super.getOrDefault(key,-1);
	}

	public void put(int key,int value){
		super.put(key,value);
	}

	@Override
	protected boolean removeEldestEntry<Map.Entry<Integer,Integer> eldest{
		return size()> capacity;
	}
}

////////////////////////////////////////////

public class LRUCache{
	class DLinkedNode{
		int key;
		int value;
		DLinkedNode prev;
		DLinkedNode next;
		public DLinkedNode(){}
		public DLinkedNode(int _key,int _value){key = _key;value = _value;}
	}

	private Map<Integer, DLinkedNode>cache = new HashMap<Integer,DLinkedNode>();
	private int size;
	private int capacity;
	private DLinkedNode head, tail;

	public int get(int key){
		DLinkedNode node = cache.get(key);
		if(node == null){
			return -1;
		}
		moveToHead(node);
		return node.value;
	}

	public void put(int key,int value){
		DLinkedNode node = cache.get(key);
		if(node == null){
			DLinkedNode newNode = new DLinkedNode(key,value);
			cache.put(key,newNode);
			addToHead(newNode);
			++size;
			if(size > capacity){
				DLinkedNode tail = removeTail();
				cache.remove(tail.key);
				--size
			}
		}ele{
			node.value = value;
			moveToHead(node);
		}
	}

	private void addToHead(DLinkedNode){
		node.prev = head;
		node.next = head.next;
		head.next.prev = node;
		head.next = node;
	}

	private void removeNode(DLinkedNode){
		node.prev.next = node.next;
		node.next.prev = node.prev;
	}

	private void moveToHead(DLinkedNode){
		removeNode(node);
		addToHead(node);
	}

	private DLinkedNode removeTail(){
		DLinkedNode res = tail.prev;
		removeNode(res);
		return res;
	}
}

时间复杂度:对于 put 和 get 都是 O(1)O(1)

空间复杂度:O(capacity)O(\text{capacity}),因为哈希表和双向链表最多存储 capacity+1\text{capacity} + 1个元素。