1. 双端队列
双端队列:是一个两端都是结尾的队列。双端队列的入队和出队操作,在两端都可以进行
它与双向链表有些相似,二者的区别在于,双端队列只能在队头和队尾进行入队、出队操作,而双向链表可以在任意位置进行结点的插入和删除
1.1 实现双端队列
1.1.1 数据结构
实现双端队列的数据结构和初始化方法:
class DoubleEndedQueue<Element>{fileprivate class Node<T> : Equatable {static func == (lhs: DoubleEndedQueue<Element>.Node<T>, rhs: DoubleEndedQueue<Element>.Node<T>) -> Bool {let obj1 = Unmanaged<AnyObject>.passUnretained(lhs).toOpaque();let obj2 = Unmanaged<AnyObject>.passUnretained(rhs).toOpaque();return obj1 == obj2;}var prev : Node<T>?;var next : Node<T>?;var data : T?;init() {}init(data : T) {self.data = data;self.prev = nil;self.next = nil;}}fileprivate var _front : Node<Element>?;fileprivate var _rear : Node<Element>?;fileprivate var _count : Int;init(){_front = Node();_rear = Node();_front?.next = _rear;_rear?.prev = _front;_count = 0;}var length : Int {get{return _count;}}func clear(){_front?.next = _rear;_rear?.prev = _front;_count = 0;}func destory(){_front = nil;_rear = nil;_count = 0;}func isEmpty() -> Bool {return _front?.next == _rear;}}
1.1.2 入队方法
实现队头和队尾的入队方法:
class DoubleEndedQueue<Element>{func enterWithFront(element : Element){let node = Node(data: element);node.next = _front?.next;node.prev = _front;_front?.next?.prev = node;_front?.next = node;_count += 1;}func enterWithRear(element : Element) {let node = Node(data: element);node.prev = _rear?.prev;node.next = _rear;_rear?.prev?.next = node;_rear?.prev = node;_count += 1;}}
1.1.3 出队方法
实现队头和队尾的出队方法:
class DoubleEndedQueue<Element>{func outWithFront() -> Element?{if(isEmpty()){return nil;}let node = _front!.next!;return out(node: node);}func outWithRear() -> Element?{if(isEmpty()){return nil;}let node = _rear!.prev!;return out(node: node);}fileprivate func out(node : Node<Element>) -> Element {node.prev?.next = node.next;node.next?.prev = node.prev;_count -= 1;return node.data!;}}
1.1.4 获取元素
分别从队头和队尾获取元素:
class DoubleEndedQueue<Element>{func peekWithFront() -> Element?{if(isEmpty()){return nil;}return _front?.next?.data;}func peekWithRear() -> Element?{if(isEmpty()){return nil;}return _rear?.prev?.data;}}
1.1.5 打印方法
实现队列的打印方法:
class DoubleEndedQueue<Element>{func traverseWithFront() -> String {var str : String = "";if(isEmpty()){return str;}var node = _front?.next;while(node != _rear){str += "\(node!.data!),"node = node?.next;}return str;}func traverseWithRear() -> String {var str : String = "";if(isEmpty()){return str;}var node = _rear?.prev;while(node != _front){str += "\(node!.data!),"node = node?.prev;}return str;}}
1.2 翻转字符串里的单词
给出一个字符串s,逐个翻转字符串中的所有单词
单词是由非空格字符组成的字符串。s中使用至少一个空格将字符串中的单词分隔开
请返回一个翻转s中单词顺序并用单个空格相连的字符串
说明:
输入字符串
s可以在前面、后面或者单词间包含多余的空格翻转后单词间应当仅用一个空格分隔
翻转后的字符串中不应包含额外的空格
示例1:
输入:s = "the sky is blue"输出:"blue is sky the"
示例2:
输入:s = " hello world "输出:"world hello"
- 输入字符串可以在前面或者后面包含多余的空格,但是翻转后的字符不能包括
示例3:
输入:s = "a good example"输出:"example good a"
- 如果两个单词间有多余的空格,将翻转后单词间的空格减少到只含一个
示例4:
输入:s = " Bob Loves Alice "输出:"Alice Loves Bob"
示例5:
输入:s = "Alice does not even like bob"输出:"bob like even not does Alice"
1.2.1 栈结构实现
遍历字符串,当前字符非空格,存储到tmp临时变量中。如果当前字符为空格,或者遍历到字符串结尾,此时tmp不为空,将其入栈,并且将tmp清空
当符合入栈条件时,tmp为空,证明遇到了连续空格,所以不需要入栈
最后进行栈的遍历,将元素依次出栈,并拼接为完整字符串,将其返回即可
代码实现:
class Solution {var stack : LinkedStack<String>;init(){stack = LinkedStack<String>();}func reverseWords(_ s: String) -> String {var tmp = "";for i in 0..<s.count {let c = s[i];if(c != " "){tmp += "\(c)";}if(!tmp.isEmpty && (c == " " || i == s.count - 1)){stack.push(element: tmp);tmp = "";}}var str = "";while(!stack.isEmpty()){if(!str.isEmpty){str += " ";}str += stack.pop()!;}return str;}}
1.2.2 双端队列实现
翻转字符串的逻辑不变,只是将栈结构换成双端队列。字符串从队尾入队,遍历元素时,再将字符串从队尾出队,达到翻转效果
代码实现:
class Solution {var queue : DoubleEndedQueue<String>;init(){queue = DoubleEndedQueue<String>();}func reverseWords(_ s: String) -> String {var tmp = "";for i in 0..<s.count {let c = s[i];if(c != " "){tmp += "\(c)";}if(!tmp.isEmpty && (c == " " || i == s.count - 1)){queue.enterWithRear(element: tmp);tmp = "";}}var str = "";while(!queue.isEmpty()){if(!str.isEmpty){str += " ";}str += queue.outWithRear()!;}return str;}}
2. 单调队列
单调队列:和普通双端队列相比,队列中的元素全部按照单调递减或递增的
单调递增队列:保证队列头元素一定是当前队列的最小值,用于维护区间的最小值
单调递减队列:保证队列头元素一定是当前队列的最大值,用于维护区间的最大值
队列的特点:队头和队尾都可以进行出队操作,但只有队尾可以进行入队操作
新元素入队时,如果从队尾插入后会破坏单调性,则删除队尾元素,直到插入后不再破坏单调性为止,再将其插入单调队列
2.1 实现单调队列
2.1.1 数据结构
实现单调队列的数据结构和初始化方法:
class MonotoneQueue<Element : Comparable>{fileprivate class Node<T> : Equatable {static func == (lhs: MonotoneQueue<Element>.Node<T>, rhs: MonotoneQueue<Element>.Node<T>) -> Bool {let obj1 = Unmanaged<AnyObject>.passUnretained(lhs).toOpaque();let obj2 = Unmanaged<AnyObject>.passUnretained(rhs).toOpaque();return obj1 == obj2;}var prev : Node<T>?;var next : Node<T>?;var data : T?;init() {}init(data : T) {self.data = data;self.prev = nil;self.next = nil;}}fileprivate var _front : Node<Element>?;fileprivate var _rear : Node<Element>?;fileprivate var _count : Int;init(){_front = Node();_rear = Node();_front?.next = _rear;_rear?.prev = _front;_count = 0;}var length : Int {get{return _count;}}func clear(){_front?.next = _rear;_rear?.prev = _front;_count = 0;}func destory(){_front = nil;_rear = nil;_count = 0;}func isEmpty() -> Bool {return _front?.next == _rear;}}
2.1.2 入队方法
实现入队方法,必须保证不能破坏单调性:
class MonotoneQueue<Element : Comparable>{func enterWithIncreasing(element : Element){while(!isEmpty()){if(peekWithRear()! <= element){break;}outWithRear();}enter(element: element);}func enterWithDecrease(element : Element) {while(!isEmpty()){if(peekWithRear()! >= element){break;}outWithRear();}enter(element: element);}fileprivate func enter(element : Element) {let node = Node(data: element);node.prev = _rear?.prev;node.next = _rear;_rear?.prev?.next = node;_rear?.prev = node;_count += 1;}}
2.1.3 出队方法
实现队头和队尾的出队方法:
class MonotoneQueue<Element : Comparable>{func outWithFront() -> Element?{if(isEmpty()){return nil;}let node = _front!.next!;return out(node: node);}func outWithRear() -> Element?{if(isEmpty()){return nil;}let node = _rear!.prev!;return out(node: node);}fileprivate func out(node : Node<Element>) -> Element {node.prev?.next = node.next;node.next?.prev = node.prev;_count -= 1;return node.data!;}}
2.1.4 获取元素
分别从队头和队尾获取元素:
class MonotoneQueue<Element : Comparable>{func peekWithFront() -> Element?{if(isEmpty()){return nil;}return _front?.next?.data;}func peekWithRear() -> Element?{if(isEmpty()){return nil;}return _rear?.prev?.data;}}
2.1.5 打印方法
实现队列的打印方法:
class MonotoneQueue<Element : Comparable>{func traverseWithFront() -> String {var str : String = "";if(isEmpty()){return str;}var node = _front?.next;while(node != _rear){str += "\(node!.data!),"node = node?.next;}return str;}func traverseWithRear() -> String {var str : String = "";if(isEmpty()){return str;}var node = _rear?.prev;while(node != _front){str += "\(node!.data!),"node = node?.prev;}return str;}}
2.2 滑动窗口最大值
给出一个整数数组nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的k个数字
滑动窗口每次只向右移动一位,我们要返回滑动窗口中的最大值
示例1:
输入:nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3输出:[3, 3, 5, 5, 6, 7]解释:滑动窗口的位置最大值----------------------------------------[1 3 -1] -3 5 3 6 7 31 [3 -1 -3] 5 3 6 7 31 3 [-1 -3 5] 3 6 7 51 3 -1 [-3 5 3] 6 7 51 3 -1 -3 [5 3 6] 7 61 3 -1 -3 5 [3 6 7] 7
示例2:
输入:nums = [1], k = 1输出:[1]
2.2.1 单调队列实现
遍历nums,将元素加入到单调递减队列中。当i >= k - 1时,说明加入队列的数据达到了窗口的限制,此时应获取队列头元素,即当前窗口中的最大值,然后将其加入到结果数组中
当窗口移动时,窗口中的数据会时时变化。所以在获得本次结果之后,应该提前删除队列中即将滑出窗口的数据。判断依据:如果队列头元素与nums[i - k + 1]相等,说明本次窗口中的最大值,就是即将滑出窗口的数据,提前将其从队列的头部出队
📢 注意:在实现单调递减队列时,我们判断队尾元素 >= 新元素即可入队。其中的>=很重要,由于元素在相等的情况下也能入队,避免了即将滑出窗口的数据和队列中下一个元素相等时的误删情况
代码实现:
class Solution {func maxSlidingWindow(_ nums: [Int], _ k: Int) -> [Int] {let queue = MonotoneQueue<Int>();var result = [Int]();for i in 0..<nums.count {queue.enterWithDecrease(element: nums[i]);if(i < k - 1){continue;}var max = queue.peekWithFront()!;result.append(max);if(max == nums[i - k + 1]){queue.outWithFront();}}return result;}}
2.2.2 双索引实现
我们可以使用monotoneQueue数组代替单调队列,定义left和right双索引,left表示当前窗口中最大值的索引值,right表示当前窗口中最小值的索引值
遍历nums,获得当前元素。遍历monotoneQueue数组,如果队尾元素小于当前元素,依次将队尾元素出队,即right -= 1。只有在队尾元素大于等于当前元素的情况下,将当前元素入队,同时right += 1
当i >= k - 1时,说明加入队列的数据达到了窗口的限制,此时应获取队列头元素,即当前窗口中的最大值,然后将其加入到结果数组中
如果队列头元素与nums[i - k + 1]相等,说明该元素即将滑出窗口,提前将其从队列的头部出队,即left += 1
📢 注意:单调队列中元素的出队条件,一定是队尾元素小于当前元素,不能使用小于等于。如果元素在相等的情况下就出队,在后面判断元素即将滑出窗口时,会造成误删除的情况
代码实现:
class Solution {func maxSlidingWindow(_ nums: [Int], _ k: Int) -> [Int] {// 记录结果的数组var result = [Int]();// 当前窗口中最大值的索引值var left = 0;// 当前窗口中最小值的索引值var right = 0;// 代替单调队列的数组var monotoneQueue = [Int].init(repeating: 0, count: nums.count);// 遍历numsfor i in 0..<nums.count {// 获得当前元素let num = nums[i];// 判断队尾元素是否小于当前元素while(left < right && monotoneQueue[right - 1] < nums[i]){// 如果小于,依次将队尾元素出队。不能破坏队列的单调性right -= 1;}// 在队尾元素大于等于当前元素的情况下,将当前元素入队monotoneQueue[right] = num;right += 1;// 判断加入队列的数据是否达到了窗口的限制if(i < k - 1){// 未达到,跳出循环continue;}// 达到窗口的限制,获取当前窗口中的最大值let max = monotoneQueue[left];// 将其加入到结果数组中result.append(max);// 判断该元素,是否为即将滑出窗口的元素if(max == nums[i - k + 1]){// 如果是,提前将其从队列的头部出队left += 1;}}return result;}}
总结
双端队列:
一个两端都是结尾的队列。双端队列的入队和出队操作,在两端都可以进行
它与双向链表有些相似,二者的区别在于,双端队列只能在队头和队尾进行入队、出队操作,而双向链表可以在任意位置进行结点的插入和删除
单调队列:
和普通双端队列相比,队列中的元素全部按照单调递减或递增的
单调递增队列:保证队列头元素一定是当前队列的最小值,用于维护区间的最小值
单调递减队列:保证队列头元素一定是当前队列的最大值,用于维护区间的最大值
队列的特点:队头和队尾都可以进行出队操作,但只有队尾可以进行入队操作
- 新元素入队时,如果从队尾插入后会破坏单调性,则删除队尾元素,直到插入后不再破坏单调性为止,再将其插入单调队列
