线性结构 一、数组(Array)
数组的砖块整齐排列,逐个紧贴。
链表的砖块分散各处,连接的藤蔓自由地穿梭于砖缝之间。
数组是一种线性数据 结构,其将相同类型的元素 存储在连续的内存空间 中。
我们将元素在数组中的位置 称为该元素的索引(index) 。
常用操作 ==int[] num = new int[]{ 1, 3, 2, 5, 4 };== ==set(int index, int value)== :给指定位置元素赋值,O(1) ==get(int index)== :获取指定位置元素,O(1) ==length()== :获取数组长度,O(1)
1.1 初始化数组 我们可以根据需求选用数组的两种初始化方式:无初始值 、给定初始值 。
1 2 3 int [] arr = new int [5 ]; int [] nums = { 1 , 3 , 2 , 5 , 4 };int [] num = new int []{ 1 , 3 , 2 , 5 , 4 };
1.2 访问元素 数组元素被存储在连续的内存空间中,计算数组元素的内存地址 非常容易。给定数组内存地址(首元素内存地址)和某个元素的索引 。
1.3 插入元素 数组元素在内存中是“紧挨着的” ,它们之间没有空间再存放任何数据 。如果想在数组中间插入一个元素,则需要将==该元素之后的所有元素都向后移动一位 ==,==之后再把元素赋值给该索引 ==。
值得注意的是,由于数组的长度是固定的 ,因此插入一个元素 必定会导致数组尾部元素“丢失” 。我们将这个问题的解决方案留在“列表”章节中讨论。
1 2 3 4 5 6 void insert (int [] nums, int num, int index) { for (int i = nums.length - 1 ; i > index; i--) { nums[i] = nums[i - 1 ]; } nums[index] = num; }
1.4 删除元素 若想删除索引 i 处的元素,则需要把索引 i 之后的元素都向前移动一位 。
1 2 3 4 5 void remove (int [] nums, int index) { for (int i = index; i < nums.length - 1 ; i++) { nums[i] = nums[i + 1 ]; } }
总的来看,数组的插入与删除操作有以下缺点:
时间复杂度高 :数组的插入和删除的平均时间复杂度均为 $O(n) $,其中$ n$ 为数组长度。
丢失元素 :由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。
内存浪费 :我们可以初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是“无意义”的,但这样做会造成部分内存空间浪费。
1.5 遍历数组 在大多数编程语言中,我们既可以通过索引遍历数组 ,也可以直接遍历获取数组中的每个元素:
1 2 3 4 5 6 7 8 9 10 11 void traverse (int [] nums) { int count = 0 ; for (int i = 0 ; i < nums.length; i++) { count += nums[i]; } for (int num : nums) { count += num; } }
1.6 查找元素 在数组中查找指定元素 需要遍历数组 ,每轮判断元素值是否匹配 ,若匹配则输出对应索引 。
因为数组是线性数据结构,所以上述查找操作被称为“线性查找”。
1 2 3 4 5 6 7 int find (int [] nums, int target) { for (int i = 0 ; i < nums.length; i++) { if (nums[i] == target) return i; } return -1 ; }
1.7 扩容数组 程序难以保证数组之后的内存空间是可用的,从而无法安全地扩展数组容量。因此在大多数编程语言中,数组的长度是不可变的 。
如果我们希望扩容数组,则需重新建立一个更大的数组 ,然后把原数组元素依次复制到新数组 。这是一个$ O(n) $的操作,在数组很大的情况下非常耗时。代码如下所示:
1 2 3 4 5 6 7 int [] extend(int [] nums, int enlarge) { int [] res = new int [nums.length + enlarge]; for (int i = 0 ; i < nums.length; i++) { res[i] = nums[i]; } return res; }
数组访问$ O(1) $快,查询删除$ O(n) $慢 。
二、动态数组(ArrayList)
我们将把“列表” 和“动态数组” 视为等同的概念。
1 2 3 4 5 private int [] elements; private E[] elements; int [] newElements = new int [newCapacity]; E[] newElements = (E[]) new Object [newCapacity];
2.1 常用操作 ==List emptyList = new ArrayList<>();== ==add(int element)== :在数组末尾添加元素,均摊 O(1) ==add(int index, int element)== :在指定索引处添加元素,平均 O(n) ==get(int index)== :获取指定索引处的元素,O(1) ==set(int index, int element)== :更新指定索引处的元素,O(1) ==remove(int index)== :移除指定索引处的元素,平均 O(n) ==removeElement(Integer element)== :移除指定元素,平均 O(n) ==size()== :获取动态数组长度,O(1) ==isEmpty()== :判断是否为空,O(1)
2.2 动态数组自实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 public class ArrayList <E> { private int size; private E[] elements; private static final int DEFAULT_CAPACITY = 10 ; private static final int ELEMENT_NOT_FOUND = -1 ; public ArrayList (int capaticy) { capaticy = (capaticy < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capaticy; elements = (E[]) new Object [capaticy]; } public ArrayList () { this (DEFAULT_CAPACITY); } public void clear () { for (int i = 0 ; i < size; i++) { elements[i] = null ; } size = 0 ; } public int size () { return size; } public boolean isEmpty () { return size == 0 ; } public boolean contains (E element) { return indexOf(element) != ELEMENT_NOT_FOUND; } public int indexOf (E element) { if (element == null ) { for (int i = 0 ; i < size; i++) { if (elements[i] == null ) return i; } } else { for (int i = 0 ; i < size; i++) { if (element.equals(elements[i])) return i; } } return ELEMENT_NOT_FOUND; } public void add (E element) { add(size, element); } public void add (int index, E element) { rangeCheckForAdd(index); ensureCapacity(size + 1 ); for (int i = size; i > index; i--) { elements[i] = elements[i - 1 ]; } elements[index] = element; size++; } public E get (int index) { rangeCheck(index); return elements[index]; } public E set (int index, E element) { rangeCheck(index); E old = elements[index]; elements[index] = element; return old; } public E remove (int index) { rangeCheck(index); E old = elements[index]; for (int i = index + 1 ; i < size; i++) { elements[i - 1 ] = elements[i]; } elements[--size] = null ; return old; } private void ensureCapacity (int capacity) { int oldCapacity = elements.length; if (oldCapacity >= capacity) return ; int newCapacity = oldCapacity + (oldCapacity >> 1 ); E[] newElements = (E[]) new Object [newCapacity]; for (int i = 0 ; i < size; i++) { newElements[i] = elements[i]; } elements = newElements; System.out.println(oldCapacity + "扩容为" + newCapacity); } private void rangeCheck (int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException ("Index:" + index + ",Size:" + size); } } private void rangeCheckForAdd (int index) { if (index < 0 || index > size) { throw new IndexOutOfBoundsException ("Index:" + index + ",Size:" + size); } } @Override public String toString () { StringBuilder string = new StringBuilder (); string.append("size=" ).append(size).append(", [" ); for (int i = 0 ; i < size; i++) { if (i != 0 ) { string.append(", " ); } string.append(elements[i]); } string.append("]" ); return string.toString(); } }
三、链表(Linkedlist) 链表是一种线性数据 结构,其中的每个元素都是 ==一个节点对象 ==,各个节点 通过“引用” 相连接。引用记录了下一个节点的内存地址 ,通过它可以从当前节点访问到下一个节点 。
链表的设计使得各个节点可以分散存储在内存各处 ,它们的内存地址无须连续 。
链表的组成单位是==节点(node)对象 ==。每个节点都包含两项数据:==节点的“值” ==和==指向下一节点的“引用” ==。
链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。
在 C、C++、Go 和 Rust 等支持指针的语言中,上述“引用”应被替换为“指针”。
链表节点 ListNode 除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,链表比数组占用更多的内存空间 。
1 2 3 4 5 6 class ListNode { int val; ListNode next; ListNode(int x) { val = x; } }
常用操作 ==List list = new LinkedList<>(); == ==add()== :在指定位置或末尾添加元素 ,平均 O(n),末尾添加 O(1) ==get()== :获取指定位置的元素,O(n) ==remove()== :移除指定位置或指定元素,平均 O(n) ,移除末尾或头节点 O(1) ==size()== :获取链表长度,O(n) ==contains()== :判断是否包含某个元素,O(n)
3.1 初始化链表 建立链表分为两步,第一步是初始化各个节点对象 ,第二步是构建节点之间的引用关系 。
初始化完成后,我们就可以从链表的头节点出发,通过引用指向 next 依次访问所有节点 。
1 2 3 4 5 6 7 8 9 10 11 12 ListNode n0 = new ListNode (1 );ListNode n1 = new ListNode (3 );ListNode n2 = new ListNode (2 );ListNode n3 = new ListNode (5 );ListNode n4 = new ListNode (4 ); n0.next = n1; n1.next = n2; n2.next = n3; n3.next = n4;
==数组整体是一个变量 ==,比如数组 nums 包含元素 nums[0] 和 nums[1] 等。
而链表是由多个独立的节点对象组成的。通常==将头节点当作链表的代称 ==,如以上代码的链表可记作链表 n0 。
3.2 插入节点 在链表中插入节点非常容易。
假设我们想在相邻的两个节点 n0 和 n1 之间插入一个新节点 P ,则只需改变两个节点引用(指针)即可 ,时间复杂度为$O(1)$。
在数组 中插入元素的时间复杂度为 $O(n)$ ,在大数据量下的效率较低 。
1 2 3 4 5 void insert (ListNode n0, ListNode P) { ListNode n1 = n0.next; P.next = n1; n0.next = P; }
3.3 删除节点 在链表中删除节点也非常方便,只需改变一个节点的引用(指针)即可 。
1 2 3 4 5 6 7 8 9 void remove (ListNode n0) { if (n0.next == null ) return ; ListNode P = n0.next; ListNode n1 = P.next; n0.next = n1; }
3.4 访问节点 在链表中访问节点的效率较低 。
在 O(1) 时间下访问数组中的任意元素。
链表则不然,程序需要从头节点出发,逐个向后遍历,直至找到目标节点 。也就是说,访问链表的第 i 个节点需要循环 i−1 轮,时间复杂度为 $O(n)$ 。
1 2 3 4 5 6 7 8 9 ListNode access (ListNode head, int index) { for (int i = 0 ; i < index; i++) { if (head == null ) return null ; head = head.next; } return head; }
3.5 查找节点 遍历链表,查找其中值为 target 的节点,输出该节点在链表中的索引。也属于线性查找。代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 int find (ListNode head, int target) { int index = 0 ; while (head != null ) { if (head.val == target) return index; head = head.next; index++; } return -1 ; }
3.6 常见的链表类型
单向链表 :即前面介绍的普通链表。单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点称为尾节点,尾节点指向空 None 。
环形链表 :如果我们令单向链表的尾节点指向头节点(首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。
双向链表 :与单向链表相比,双向链表记录了两个方向的引用。双向链表的节点定义同时包含指向后继节点(下一个节点)和前驱节点(上一个节点)的引用(指针)。相较于单向链表,双向链表更具灵活性,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。
1 2 3 4 5 6 7 class ListNode { int val; ListNode next; ListNode prev; ListNode(int x) { val = x; } }
3.7 单向链表的自实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 public class SinglyLinkedList <E>{ private int size; private Node<E> first; static final int ELEMENT_NOT_FOUND = -1 ; private static class Node <E>{ E element; Node<E> next; public Node (E element, Node<E> next) { this .element = element; this .next = next; } } public void clear () { size = 0 ; first = null ; } public E get (int index) { return node(index).element; } private Node<E> node (int index) { rangeCheck(index); Node<E> node = first; for (int i = 0 ; i < index; i++) { node = node.next; } return node; } public E set (int index, E element) { Node<E> node = node(index); E old = node.element; node.element = element; return old; } public void add (int index, E element) { rangeCheckForAdd(index); if (index == 0 ){ first = new Node <>(element, first); } else { Node<E> prev = node(index - 1 ); prev.next = new Node <>(element, prev.next); } size++; } public E remove (int index) { rangeCheck(index); Node<E> node = first; if (index == 0 ) { first = first.next; } else { Node<E> prev = node(index - 1 ); node = prev.next; prev.next = node.next; } size--; return node.element; } public int indexOf (E element) { if (element == null ){ Node<E> node = first; for (int i = 0 ; i < size; i++) { if (node.element == null ) return i; node = node.next; } } else { Node<E> node = first; for (int i = 0 ; i < size; i++) { if (element.equals(node.element)) return i; node = node.next; } } return ELEMENT_NOT_FOUND; } public String toString () { StringBuilder string = new StringBuilder (); string.append("size=" ).append(size).append(", [" ); Node<E> node = first; for (int i = 0 ; i < size; i++) { if (i != 0 ) { string.append(", " ); } string.append(node.element); node = node.next; } string.append("]" ); return string.toString(); } private void rangeCheck (int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException ("Index: " + index + ", Size: " + size); } } private void rangeCheckForAdd (int index) { if (index < 0 || index > size) { throw new IndexOutOfBoundsException ("Index: " + index + ", Size: " + size); } } }
单向链表 :即前面介绍的普通链表。单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点称为尾节点,尾节点指向空 None 。
环形链表 :如果我们令单向链表的尾节点指向头节点(首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。
双向链表 :与单向链表相比,双向链表记录了两个方向的引用。双向链表的节点定义同时包含指向后继节点(下一个节点)和前驱节点(上一个节点)的引用(指针)。相较于单向链表,双向链表更具灵活性,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。
单向链表通常用于实现栈、队列、哈希表和图 等数据结构。
3.8 双向链表的自实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 public class LinkedList <E> { private Node<E> first; private Node<E> last; private static class Node <E> { E element; Node<E> prev; Node<E> next; public Node (Node<E> prev, E element, Node<E> next) { this .prev = prev; this .element = element; this .next = next; } public String toString () { StringBuilder sb = new StringBuilder (); if (prev != null ) { sb.append(prev.element); } else { sb.append("null" ); } sb.append("_" ).append(element).append("_" ); if (next != null ) { sb.append(next.element); } else { sb.append("null" ); } return sb.toString(); } } public void clear () { size = 0 ; first = null ; last = null ; } public E get (int index) { return node(index).element; } private Node<E> node (int index) { rangeCheck(index); if (index < (size >> 1 )) { Node<E> node = first; for (int i = 0 ; i < index; i++) { node = node.next; } return node; } else { Node<E> node = last; for (int i = size - 1 ; i > index; i--) { node = node.prev; } return node; } } public E set (int index, E element) { Node<E> node = node(index); E old = node.element; node.element = element; return old; } public void add (int index, E element) { rangeCheckForAdd(index); if (index == size) { Node<E> oldLast = last; last = new Node <>(oldLast, element, null ); if (oldLast == null ) { first = last; } else { oldLast.next = last; } } else { Node<E> next = node(index); Node<E> prev = next.prev; Node<E> node = new Node <>(prev, element, next); next.prev = node; if (prev == null ) { first = node; } else { prev.next = node; } } size++; } public E remove (int index) { rangeCheck(index); Node<E> node = node(index); Node<E> prev = node.prev; Node<E> next = node.next; if (prev == null ) { first = next; } else { prev.next = next; } if (next == null ) { last = prev; } else { next.prev = prev; } size--; return node.element; } public int indexOf (E element) { if (element == null ) { Node<E> node = first; for (int i = 0 ; i < size; i++) { if (node.element == null ) return i; node = node.next; } } else { Node<E> node = first; for (int i = 0 ; i < size; i++) { if (element.equals(node.element)) return i; node = node.next; } } return ELEMENT_NOT_FOUND; } @Override public String toString () { StringBuilder string = new StringBuilder (); string.append("size=" ).append(size).append(", [" ); Node<E> node = first; for (int i = 0 ; i < size; i++) { if (i != 0 ) { string.append(", " ); } string.append(node); node = node.next; } string.append("]" ); return string.toString(); } }
9.循环链表的自实现
四、栈(Stack) 栈如同叠猫猫 ,而队列就像猫猫排队 。
栈(stack)是一种遵循先入后出 逻辑的线性数据结构。
我们把堆叠元素的顶部 称为“栈顶” ,底部称为“栈底” 。
将把元素添加到栈顶 的操作叫作“入栈” ,删除栈顶元素 的操作叫作“出栈” 。
4.1 栈的常用操作 ==Stack stack = new Stack<>(); ==
==push(int element) ==:元素入栈(添加至栈顶),$ O(1)$
==pop() ==:栈顶元素出栈,$ O(1)$
==peek() ==:访问栈顶元素,$ O(1) $。
==size() ==:获取长度
==isEmpty() ==:是否为空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Stack<Integer> stack = new Stack <>(); stack.push(1 ); stack.push(3 ); stack.push(2 ); stack.push(5 ); stack.push(4 );int peek = stack.peek();int pop = stack.pop();int size = stack.size();boolean isEmpty = stack.isEmpty();
4.2 基于链表实现栈 使用链表实现栈时,我们可以将==链表的头节点 视为栈顶 ==,==尾节点 视为栈底 ==。
对于入栈操作,只需将元素插入链表头部 ,这种节点插入方法被称为“头插法”。
对于出栈操作,只需将头节点从链表中删除 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class LinkedListStack { private ListNode stackPeek; private int stkSize = 0 ; public LinkedListStack () { stackPeek = null ; } public int size () { return stkSize; } public boolean isEmpty () { return size() == 0 ; } public void push (int num) { ListNode node = new ListNode (num); node.next = stackPeek; stackPeek = node; stkSize++; } public int pop () { int num = peek(); stackPeek = stackPeek.next; stkSize--; return num; } public int peek () { if (isEmpty()) throw new IndexOutOfBoundsException (); return stackPeek.val; } public int [] toArray() { ListNode node = stackPeek; int [] res = new int [size()]; for (int i = res.length - 1 ; i >= 0 ; i--) { res[i] = node.val; node = node.next; } return res; } }
浏览器中的后退与前进、软件中的撤销与反撤销。
4.3 基于数组实现栈 使用数组实现栈时,我们可以将数组的尾部作为栈顶 。入栈与出栈操作分别对应在数组尾部添加元素与删除元素 ,时间复杂度都为$ O(1) $。
由于入栈的元素可能会源源不断地增加 ,因此我们可以使用动态数组 ,这样就无须自行处理数组扩容问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class ArrayListStack { private ArrayList<Integer> stack; public ArrayListStack () { stack = new ArrayList <>(); } public int size () { return stack.size(); } public boolean isEmpty () { return size() == 0 ; } public void push (int num) { stack.add(num); } public int pop () { if (isEmpty()) throw new IndexOutOfBoundsException (); return stack.remove(size() - 1 ); } public int peek () { if (isEmpty()) throw new IndexOutOfBoundsException (); return stack.get(size() - 1 ); } public Object[] toArray() { return stack.toArray(); } }
数组实现额外支持随机访问,但这已超出了栈的定义范畴,因此一般不会用到。
五、队列(Queue) 队列(queue)是一种遵循先入先出规则的线性数据结构。
队列模拟了排队现象。新来的人不断加入队列尾部,而位于队列头部的人逐个离开。
队列头部称为“队首” ,尾部称为“队尾” ,把元素加入队尾的操作称为“入队” ,删除队首元素的操作称为“出队” 。
5.1 队列的常用操作 ==Queue queue = new LinkedList<>(); ==
==offer(int element) ==:元素入队,即将元素添加至队尾,$ O(1)$
==poll() ==:队首元素出队,$ O(1)$
==peek() ==:访问队首元素,$ O(1) $
==size() ==:获取长度
==isEmpty() ==:是否为空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Queue<Integer> queue = new LinkedList <>(); queue.offer(1 ); queue.offer(3 ); queue.offer(2 ); queue.offer(5 ); queue.offer(4 );int peek = queue.peek();int pop = queue.poll();int size = queue.size();boolean isEmpty = queue.isEmpty();
5.2 基于链表实现队列 我们可以将链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class LinkedListQueue { private ListNode front, rear; private int queSize = 0 ; public LinkedListQueue () { front = null ; rear = null ; } public int size () { return queSize; } public boolean isEmpty () { return size() == 0 ; } public void push (int num) { ListNode node = new ListNode (num); if (front == null ) { front = node; rear = node; } else { rear.next = node; rear = node; } queSize++; } public int pop () { int num = peek(); front = front.next; queSize--; return num; } public int peek () { if (isEmpty()) throw new IndexOutOfBoundsException (); return front.val; } public int [] toArray() { ListNode node = front; int [] res = new int [size()]; for (int i = 0 ; i < res.length; i++) { res[i] = node.val; node = node.next; } return res; } }
5.3 双向队列(Deque) 在队列中,我们仅能删除头部元素 或在尾部添加元素 。
双向队列提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作 。
方法名
功能
时间复杂度
==offerFirst() ==
将元素添加至队首
O(1)
==offerLast() ==
将元素添加至队尾
O(1)
==pollFirst() ==
删除队首元素
O(1)
==pollLast() ==
删除队尾元素
O(1)
==peekFirst() ==
访问队首元素
O(1)
==peekLast() ==
访问队尾元素
O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Deque<Integer> deque = new LinkedList <>(); deque.offerLast(2 ); deque.offerLast(5 ); deque.offerLast(4 ); deque.offerFirst(3 ); deque.offerFirst(1 );int peekFirst = deque.peekFirst(); int peekLast = deque.peekLast(); int popFirst = deque.pollFirst(); int popLast = deque.pollLast(); int size = deque.size();boolean isEmpty = deque.isEmpty();
双向队列 兼具栈与队列 的逻辑,因此它可以实现这两者的所有应用场景 ,同时提供更高的自由度。
我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作push 到栈中,然后通过pop 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存50 步)。当栈的长度超过50 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。
注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。