数据结构学习-线性结构(数组、链表、栈、队列)
线性结构介绍:
线性结构是数据结构中的一种基本类型,它是一种数据元素之间存在一对一关系的数据结构。在线性结构中,每个元素(除了第一个和最后一个)都有一个直接前驱和一个直接后继。常见的线性结构有数组、链表、栈和队列等。
1.数组(Array)
描述:
数组是一种线性数据结构,用于存储固定大小的有序集合。数组中的元素可以通过索引直接访问,这意味着访问速度非常快。数组可以在静态分配或动态分配中创建,具体取决于编程语言和应用场景。
优点:
- 快速访问:通过索引可以直接访问元素,时间复杂度为O(1)。
- 缓存友好:由于数组元素在内存中是连续存储的,因此访问相邻元素时可以利用CPU缓存,提高性能。
缺点:
- 固定大小:一旦创建了数组,其大小就不能改变,这限制了灵活性。
- 插入和删除效率低:在数组中间插入或删除元素时,需要移动后续元素,时间复杂度为O(n)。
面试题:
题目:如何在已排序的数组中查找一个元素?
解答:可以使用二分查找算法,时间复杂度为O(log n)。
代码说明:
public class BinarySearch {
// 二分查找方法
public static int binarySearch(int[] array, int target) {
int left = 0; // 左边界
int right = array.length - 1; // 右边界
while (left <= right) {
int mid = left + (right - left) / 2; // 计算中间位置,防止溢出
if (array[mid] == target) {
return mid; // 找到目标元素,返回其索引
} else if (array[mid] < target) {
left = mid + 1; // 目标在右半部分,更新左边界
} else {
right = mid - 1; // 目标在左半部分,更新右边界
}
}
return -1; // 没有找到目标元素
}
public static void main(String[] args) {
int[] sortedArray = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 已排序的数组
int target = 5; // 要查找的目标元素
int index = binarySearch(sortedArray, target); // 调用二分查找方法
if (index != -1) {
System.out.println("Element found at index: " + index); // 找到目标元素,输出索引
} else {
System.out.println("Element not found in the array."); // 没有找到目标元素
}
}
}
题目:如何在数组中实现动态扩容?
解答:当数组空间不足时,可以创建一个新的更大的数组,将原数组中的元素复制到新数组中,然后释放旧数组的空间。
代码说明:
public class DynamicArray {
private int[] array; // 存储数组
private int size; // 当前数组中的元素数量
private int capacity; // 数组的容量
// 构造函数,初始化数组的初始容量
public DynamicArray(int initialCapacity) {
this.capacity = initialCapacity;
this.array = new int[initialCapacity];
this.size = 0;
}
// 添加元素到数组中
public void add(int value) {
if (size == capacity) {
resize(); // 如果当前容量已满,进行扩容
}
array[size++] = value; // 将新元素添加到数组末尾,并增加元素数量
}
// 扩容方法
private void resize() {
int newCapacity = capacity * 2; // 新容量为当前容量的两倍
int[] newArray = new int[newCapacity]; // 创建新的数组
// 将原数组中的元素复制到新数组中
for (int i = 0; i < size; i++) {
newArray[i] = array[i];
}
array = newArray; // 更新引用,指向新数组
capacity = newCapacity; // 更新容量
}
public static void main(String[] args) {
DynamicArray dynamicArray = new DynamicArray(3); // 初始化一个容量为3的动态数组
dynamicArray.add(1); // 添加元素
dynamicArray.add(2);
dynamicArray.add(3);
dynamicArray.add(4); // 触发扩容
// 输出数组中的所有元素
for (int i = 0; i < dynamicArray.size; i++) {
System.out.println("Element at index " + i + ": " + dynamicArray.array[i]);
}
}
}
2. 链表(Linked List)
描述:
链表是一种线性数据结构,其中的元素(节点)通过指针连接在一起。每个节点包含数据部分和指向下一个节点的指针。链表可以是单向链表、双向链表或循环链表。
优点:
- 动态大小:链表可以在运行时动态增加或减少节点,灵活性高。
- 高效的插入和删除:在链表中插入或删除节点只需要修改指针,时间复杂度为O(1)。
缺点:
- 访问效率低:链表不能通过索引直接访问元素,需要从头节点开始逐个遍历,时间复杂度为O(n)。
- 额外的内存开销:每个节点除了存储数据外,还需要存储指针,增加了内存开销。
面试题:
题目:如何反转一个单向链表?
解答:可以使用迭代或递归的方法来反转链表。迭代方法的时间复杂度为O(n),空间复杂度为O(1)。
代码说明:
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public class ReverseLinkedList {
// 反转链表方法
public ListNode reverseList(ListNode head) {
ListNode prev = null; // 前一个节点
ListNode current = head; // 当前节点
while (current != null) {
ListNode next = current.next; // 保存当前节点的下一个节点
current.next = prev; // 将当前节点的 next 指向前一个节点
prev = current; // 更新前一个节点为当前节点
current = next; // 移动到下一个节点
}
return prev; // 返回新的头节点
}
public static void main(String[] args) {
// 创建一个链表 1 -> 2 -> 3 -> 4
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
ReverseLinkedList solution = new ReverseLinkedList();
ListNode reversedHead = solution.reverseList(head); // 调用反转方法
// 输出反转后的链表
while (reversedHead != null) {
System.out.print(reversedHead.val + " ");
reversedHead = reversedHead.next;
}
}
}
题目:如何检测链表中是否存在环?
解答:可以使用快慢指针法。快指针每次移动两步,慢指针每次移动一步。如果链表中存在环,快指针最终会追上慢指针。
代码说明:
public class DetectCycle {
// 检测链表中是否存在环
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false; // 链表为空或只有一个节点,不可能有环
}
ListNode slow = head; // 慢指针,每次移动一步
ListNode fast = head.next; // 快指针,每次移动两步
while (fast != null && fast.next != null) {
if (slow == fast) {
return true; // 快指针追上慢指针,说明有环
}
slow = slow.next; // 慢指针移动一步
fast = fast.next.next; // 快指针移动两步
}
return false; // 快指针到达链表末尾,没有环
}
public static void main(String[] args) {
// 创建一个链表 1 -> 2 -> 3 -> 4 -> 2 (形成环)
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = head.next; // 创建环
DetectCycle solution = new DetectCycle();
boolean hasCycle = solution.hasCycle(head); // 调用检测环的方法
System.out.println("Has cycle: " + hasCycle); // 输出是否有环
}
}
3. 栈(Stack)
描述:
栈是一种特殊的线性表,只允许在一端进行插入或删除操作。栈遵循“后进先出”(LIFO)原则。
优点:
- 操作简单:栈的基本操作(入栈、出栈、读取栈顶元素)都非常简单,容易实现。
- 支持回溯:栈可以用于支持函数调用栈、表达式求值等需要回溯的场景。
缺点:
- 容量限制:如果使用固定大小的数组实现栈,可能会遇到栈溢出的问题。
- 功能单一:栈只能进行栈顶操作,不适合需要随机访问或中间操作的场景。
面试题:
题目:如何使用两个栈实现一个队列?
解答:可以使用两个栈stack1和stack2来实现队列。入队时将元素压入stack1,出队时将stack1中的元素依次弹出并压入stack2,然后从stack2中弹出元素。
代码说明:
import java.util.Stack;
class MyQueue {
private Stack<Integer> stack1; // 用于入队操作
private Stack<Integer> stack2; // 用于出队操作
// 构造函数,初始化两个栈
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
// 入队操作,将元素压入 stack1
public void push(int x) {
stack1.push(x);
}
// 出队操作,将 stack1 中的元素依次移到 stack2,然后从 stack2 弹出元素
public int pop() {
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
// 获取队头元素,类似于 pop 操作,但不移除元素
public int peek() {
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
// 判断队列是否为空
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
public static void main(String[] args) {
MyQueue queue = new MyQueue();
queue.push(1); // 入队
queue.push(2);
queue.push(3);
System.out.println("Front element: " + queue.peek()); // 输出队头元素
System.out.println("Dequeued element: " + queue.pop()); // 出队
System.out.println("Is queue empty? " + queue.empty()); // 判断队列是否为空
}
}
4. 队列(Queue)
描述:
队列是一种特殊的线性表,允许在一端进行插入操作,在另一端进行删除操作。队列遵循“先进先出”(FIFO)原则。
优点:
- 公平性:队列保证了元素的处理顺序,适用于需要按顺序处理任务的场景。
- 简单直观:队列的操作简单直观,容易理解和实现。
缺点:
- 固定大小:如果使用固定大小的数组实现队列,可能会遇到队列满的问题。
- 插入和删除操作受限:队列只能在两端进行操作,不支持中间操作。
面试题:
题目:如何使用两个队列实现一个栈?
解答:可以使用两个队列queue1和queue2来实现栈。入栈时将元素加入queue1,出栈时将queue1中的元素依次移到queue2,直到只剩下一个元素,然后将该元素出队并返回。最后交换queue1和queue2的角色。
代码说明:
import java.util.LinkedList;
import java.util.Queue;
class MyStack {
private Queue<Integer> queue1; // 主队列,用于存储元素
private Queue<Integer> queue2; // 辅助队列,用于临时存储元素
// 构造函数,初始化两个队列
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
// 入栈操作,将元素加入 queue1
public void push(int x) {
queue1.offer(x);
}
// 出栈操作,将 queue1 中的元素依次移到 queue2,直到只剩下一个元素,然后从 queue1 弹出该元素
public int pop() {
while (queue1.size() > 1) {
queue2.offer(queue1.poll());
}
int top = queue1.poll();
swapQueues();
return top;
}
// 获取栈顶元素,类似于 pop 操作,但不移除元素
public int top() {
while (queue1.size() > 1) {
queue2.offer(queue1.poll());
}
int top = queue1.poll();
queue2.offer(top); // 将栈顶元素重新加入 queue2
swapQueues();
return top;
}
// 判断栈是否为空
public boolean empty() {
return queue1.isEmpty();
}
// 交换两个队列的角色
private void swapQueues() {
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}
public static void main(String[] args) {
MyStack stack = new MyStack();
stack.push(1); // 入栈
stack.push(2);
stack.push(3);
System.out.println("Top element: " + stack.top()); // 输出栈顶元素
System.out.println("Popped element: " + stack.pop()); // 出栈
System.out.println("Is stack empty? " + stack.empty()); // 判断栈是否为空
}
}
1682

被折叠的 条评论
为什么被折叠?



