八股文之快速上手数据结构与算法

791 阅读28分钟

在计算机领域的面试中,经常会涉及到一些基础知识和常见问题,这些问题通常都是固定的,也被称为“八股文”。以下是一些常见的计算机面试八股文:

1. 数据结构与算法:包括数组、链表、栈、队列、树、图等基本数据结构以及排序、查找、遍历等常见算法。

2. 操作系统:包括进程、线程、调度算法、死锁、内存管理、虚拟内存、文件系统等操作系统的基本概念和实现原理。

3. 计算机网络:包括TCP/IP协议、HTTP协议、网络安全、路由、交换等网络基础知识和常见问题。

4. 数据库:包括关系型数据库、SQL语言、索引、事务、并发控制等数据库基础知识和常见问题。

5. 编程语言:包括Java、C++、Python等编程语言的基本语法、面向对象编程、异常处理、多线程编程等基础知识和常见问题。

6. 设计模式:包括单例模式、工厂模式、观察者模式等常见的设计模式及其应用。

7. Web开发:包括HTML、CSS、JavaScript、AJAX、jQuery等Web开发技术和常见问题。

8. 框架和工具:包括Spring、Hibernate、MyBatis、Maven、Git等常见的框架和工具的使用和原理。

本节先从数据结构与算法开始

当谈到数据结构和算法时,以下是一些常见的基本数据结构和算法:

1. 数组:数组是一种线性数据结构,其中元素按照顺序存储。数组的特点是可以通过索引访问任何元素,但在插入和删除元素时需要进行大量的数据移动。以下是一个整数数组的Java代码示例:

int[] arr = {10, 20, 30, 40, 50};

以下是该数组的示意图:

   +----+----+----+----+----+
   | 10 | 20 | 30 | 40 | 50 |
   +----+----+----+----+----+

2. 链表(Linked List)是一种基于指针的动态数据结构,它由若干个节点(Node)组成,每个节点包含两个部分:数据域(Data)和指针域(Next)。数据域存储节点的数据,指针域存储下一个节点的地址。通过指针将所有节点连接起来,形成链式结构。

链表可以分为单向链表和双向链表,单向链表每个节点只有一个指针域,指向下一个节点;双向链表每个节点有两个指针域,分别指向前一个节点和后一个节点。双向链表相对于单向链表来说,可以实现双向遍历,但是需要更多的空间存储指针域。

链表相对于数组来说,具有动态性,可以根据需要动态地分配和释放内存空间,但是访问元素需要遍历链表,效率较低。

链表的基本操作包括插入、删除、查找等。以下是基于单向链表实现的Java代码示例:

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

class LinkedList {
    private ListNode head;

    public LinkedList() {
        head = null;
    }

    public void addFirst(int val) {
        ListNode node = new ListNode(val);
        node.next = head;
        head = node;
    }

    public void addLast(int val) {
        ListNode node = new ListNode(val);
        if (head == null) {
            head = node;
        } else {
            ListNode curr = head;
            while (curr.next != null) {
                curr = curr.next;
            }
            curr.next = node;
        }
    }

    public void add(int index, int val) {
        if (index < 0 || index > size()) {
            throw new RuntimeException("Index out of range");
        }
        if (index == 0) {
            addFirst(val);
        } else if (index == size()) {
            addLast(val);
        } else {
            ListNode node = new ListNode(val);
            ListNode curr = head;
            for (int i = 0; i < index - 1; i++) {
                curr = curr.next;
            }
            node.next = curr.next;
            curr.next = node;
        }
    }

    public void removeFirst() {
        if (head == null) {
            throw new RuntimeException("List is empty");
        }
        head = head.next;
    }

    public void removeLast() {
        if (head == null) {
            throw new RuntimeException("List is empty");
        }
        if (head.next == null) {
            head = null;
        } else {
            ListNode curr = head;
            while (curr.next.next != null) {
                curr = curr.next;
            }
            curr.next = null;
        }
    }

    public void remove(int index) {
        if (index < 0 || index >= size()) {
            throw new RuntimeException("Index out of range");
        }
        if (index == 0) {
            removeFirst();
        } else if (index == size() - 1) {
            removeLast();
        } else {
            ListNode curr = head;
            for (int i = 0; i < index - 1; i++) {
                curr = curr.next;
            }
            curr.next = curr.next.next;
        }
    }

    public int get(int index) {
        if (index < 0 || index >= size()) {
            throw new RuntimeException("Index out of range");
        }
        ListNode curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr.val;
    }

    public boolean contains(int val) {
        ListNode curr = head;
        while (curr != null) {
            if (curr.val == val) {
                return true;
            }
            curr = curr.next;
        }
        return false;
    }

    public int size() {
        int size = 0;
        ListNode curr = head;
        while (curr != null) {
            size++;
            curr = curr.next;
        }
        return size;
    }
}

以下是对该链表的示意图:

   head -> +------+    +------+    +------+
            |  val | -> |  val | -> |  val |
            +------+    +------+    +------+

当元素 4 插入到索引 1 处时,链表的状态如下:

   head -> +------+    +------+    +------+    +------+
            |  val | -> |   4  | -> |  val | -> |  val |
            +------+    +------+    +------+    +------+

当元素 6 删除时,链表的状态如下:

   head -> +------+    +------+    +------+
            |  val | -> |  val | -> |  val |
            +------+    +------+    +------+

以下是基于双向链表实现的Java代码示例:

class ListNode {
    int val;
    ListNode prev;
    ListNode next;
    ListNode(int val) {
        this.val = val;
        this.prev = null;
        this.next = null;
    }
}

class DoublyLinkedList {
    private ListNode head;
    private ListNode tail;

    public DoublyLinkedList() {
        head = null;
        tail = null;
    }

    public void addFirst(int val) {
        ListNode node = new ListNode(val);
        if (head == null) {
            head = node;
            tail = node;
        } else {
            node.next = head;
            head.prev = node;
            head = node;
        }
    }

    public void addLast(int val) {
        ListNode node = new ListNode(val);
        if (head == null) {
            head = node;
            tail = node;
        } else {
            node.prev = tail;
            tail.next = node;
            tail = node;
        }
    }

    public void add(int index, int val) {
        if (index < 0 || index > size()) {
            throw new RuntimeException("Index out of range");
        }
        if (index == 0) {
            addFirst(val);
        } else if (index == size()) {
            addLast(val);
        } else {
            ListNode node = new ListNode(val);
            ListNode curr = head;
            for (int i = 0; i < index - 1; i++) {
                curr = curr.next;
            }
            node.prev = curr;
            node.next = curr.next;
            curr.next.prev = node;
            curr.next = node;
        }
    }

    public void removeFirst() {
        if (head == null) {
            throw new RuntimeException("List is empty");
        }
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            head = head.next;
            head.prev = null;
        }
    }

    public void removeLast() {
        if (head == null) {
            throw new RuntimeException("List is empty");
        }
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            tail = tail.prev;
            tail.next = null;
        }
    }

    public void remove(int index) {
        if (index < 0 || index >= size()) {
            throw new RuntimeException("Index out of range");
        }
        if (index == 0) {
            removeFirst();
        } else if (index == size() - 1) {
            removeLast();
        } else {
            ListNode curr = head;
            for (int i = 0; i < index; i++) {
                curr = curr.next;
            }
            curr.prev.next = curr.next;
            curr.next.prev = curr.prev;
        }
    }

    public int get(int index) {
        if (index < 0 || index >= size()) {
            throw new RuntimeException("Index out of range");
        }
        ListNode curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr.val;
    }

    public boolean contains(int val) {
        ListNode curr = head;
        while (curr != null) {
            if (curr.val == val) {
                return true;
            }
            curr = curr.next;
        }
        return false;
    }

    public int size() {
        int size = 0;
        ListNode curr = head;
        while (curr != null) {
            size++;
            curr = curr.next;
        }
        return size;
    }
}

以下是对该双向链表的示意图:

   head -> +------+    +------+    +------+    +------+
            |  val | <-> |  val | <-> |  val | <-> |  val |
   tail <- +------+    +------+    +------+    +------+

当元素 4 插入到索引 1 处时,链表的状态如下:

   head -> +------+    +------+    +------+    +------+    +------+
            |  val | <-> |   4  | <-> |  val | <-> |  val | <-> |  val |
   tail <- +------+    +------+    +------+    +------+    +------+

当元素 6 删除时,链表的状态如下:

   head -> +------+    +------+    +------+    +------+
            |  val | <-> |  val | <-> |  val | <-> |  val |
   tail <- +------+    +------+    +------+    +------+

3. 栈(Stack是一种基于后进先出(Last-In-First-Out,LIFO)策略的线性数据结构,它可以在一端进行插入和删除操作,这一端被称为栈顶(Top),另一端被称为栈底(Bottom)。栈的插入操作称为入栈(Push),删除操作称为出栈(Pop)。由于栈的特殊性质,它通常用于需要后进先出的场景,例如函数调用栈、表达式求值、括号匹配等。

栈可以用数组或链表实现。使用数组实现的栈称为顺序栈,使用链表实现的栈称为链式栈。顺序栈的优点是访问元素快速,缺点是容量固定,需要预先分配空间。链式栈的优点是容量可以动态增长,缺点是访问元素需要遍历链表,效率较低。

栈的基本操作包括入栈、出栈、获取栈顶元素、判断栈是否为空等。以下是基于数组实现的顺序栈的Java代码示例:

class ArrayStack {
    private int[] data;
    private int top;

    public ArrayStack(int capacity) {
        data = new int[capacity];
        top = -1;
    }

    public void push(int x) {
        if (top == data.length - 1) {
            throw new RuntimeException("Stack is full");
        }
        data[++top] = x;
    }

    public int pop() {
        if (top == -1) {
            throw new RuntimeException("Stack is empty");
        }
        return data[top--];
    }

    public int peek() {
        if (top == -1) {
            throw new RuntimeException("Stack is empty");
        }
        return data[top];
    }

    public boolean isEmpty() {
        return top == -1;
    }
}

以下是对该栈的示意图:

   +----+----+----+----+----+
   | 10 | 20 | 30 |    |    |
   +----+----+----+----+----+
   top = 2

当元素 40 入栈时,栈的状态如下:

   +----+----+----+----+----+
   | 10 | 20 | 30 | 40 |    |
   +----+----+----+----+----+
   top = 3

当元素出栈时,栈的状态如下:

   +----+----+----+----+----+
   | 10 | 20 | 30 |    |    |
   +----+----+----+----+----+
   top = 2

当获取栈顶元素时,返回的是 30。

4. 队列:队列是一种先进先出(FIFO)的数据结构,其中元素按照顺序存储。队列的特点是只能在队尾插入元素,在队头删除元素,但在访问元素时不需要遍历整个队列。以下是一个整数队列的Java代码示例:

Queue<Integer> queue = new LinkedList<>();
queue.offer(10);
queue.offer(20);
queue.offer(30);
int front = queue.poll();

以下是该队列的示意图:

   +------+------+------+
   |  20  |  30  | null |
   +------+------+------+
   front           rear

5. 树:树(Tree)是一种非线性数据结构,它由节点(Node)和边(Edge)组成。树的节点可以有零个或多个子节点,除了根节点(Root),每个节点都有一个父节点(Parent)。树的边表示节点之间的关系,每个节点最多只有一个父节点,但可以有多个子节点。

树是一种递归定义的数据结构,它可以被定义为一个空树(Empty Tree),或者一个根节点和若干个子树的集合。每个子树也是一棵树。

树的一些基本术语包括:

  • 根节点(Root):树的顶层节点,没有父节点。
  • 叶子节点(Leaf):没有子节点的节点。
  • 父节点(Parent):有子节点的节点。
  • 子节点(Child):一个节点的子树中的节点。
  • 兄弟节点(Sibling):具有相同父节点的节点。
  • 子树(Subtree):一个节点和它的所有后代节点构成的树。
  • 深度(Depth):从根节点到该节点的路径长度。
  • 高度(Height):从该节点到其最远叶子节点的路径长度。

树的应用非常广泛,例如在计算机科学中,树被用来表示文件系统、XML文档、数据库索引等。常见的树包括二叉树(Binary Tree)、二叉搜索树(Binary Search Tree)、平衡二叉树(Balanced Binary Tree)、红黑树(Red-Black Tree)等。

以下是一个二叉树的示意图:

        1
       / \
      2   3
     / \   \
    4   5   6

在这个二叉树中,1 是根节点,2 和 3 是它的子节点,4 和 5 是 2 的子节点,6 是 3 的子节点。节点 4、5、6 都是叶子节点,它们没有子节点。节点 1 的深度为 0,节点 2 和 3 的深度为 1,节点 4、5 和 6 的深度为 2。节点 4 和 5 的高度为 0,节点 2 和 3 的高度为 1,节点 1 的高度为 2。

6. 图:图(Graph)是一种非线性数据结构,它由节点(Vertex)和边(Edge)组成。图中的节点可以有零个或多个相邻节点,相邻节点之间由边连接。边可以是有向的或无向的,有向边有一个方向,表示从一个节点到另一个节点的方向,而无向边没有方向,表示两个节点之间的关系是相互的。

图可以被表示为一个二元组 G=(V,E) ,其中 V 表示节点的集合,E 表示边的集合。如果图是有向的,则每个边是一个有序对 (u,v),表示从节点 u 到节点 v 有一条有向边。如果图是无向的,则每个边是一个无序对 {u,v},表示节点 u 和节点 v 之间有一条无向边。

图的一些基本术语包括:

  • 顶点(Vertex):图中的节点。
  • 边(Edge):图中节点之间的连接。
  • 相邻节点(Adjacent Vertex):有一条边连接的节点。
  • 有向图(Directed Graph):边有方向的图。
  • 无向图(Undirected Graph):边没有方向的图。
  • 权重(Weight):边上的数值,表示两个节点之间的距离或成本。
  • 路径(Path):从一个节点到另一个节点的边序列。
  • 环(Cycle):至少有一条边和起点相同的路径。
  • 连通图(Connected Graph):图中任意两个节点之间都存在路径的图。
  • 子图(Subgraph):从原图中选取一些节点和边组成的图。

图的应用非常广泛,例如在计算机科学中,图被用于表示网络、社交网络、地图等。常见的图算法包括深度优先搜索(DFS)、广度优先搜索(BFS)、最短路径算法、最小生成树算法等。

以下是一个有向图的示意图:

   1 -> 2 -> 3
   ^    |    |
   |    v    v
   4 <- 5 <- 6

在这个有向图中,1、2、3、4、5、6 是节点,箭头表示有向边。例如,从节点 2 到节点 5 有一条有向边,从节点 5 到节点 1 也有一条有向边。节点 1、2、3、4、5、6 组成了图的节点集合 V,有向边组成了图的边集合 E。

7. 排序算法:

排序算法是一种将一组数据按照一定顺序进行排列的算法。排序算法是计算机科学中非常基础的算法之一,它在实际应用中有着广泛的应用,例如在搜索、数据库、图形学、机器学习等领域都有着重要的应用。

排序算法通常根据其时间复杂度、空间复杂度、稳定性、适用场景等方面进行分类。常见的排序算法包括:

  1. 冒泡排序(Bubble Sort):比较相邻元素的大小,将较大的元素向后移动,最终将最大的元素移到数组末尾,重复这个过程直到整个数组有序。时间复杂度为 O(n^2)。
  2. 选择排序(Selection Sort):从未排序的部分中选择最小的元素放到已排序的部分的末尾,重复这个过程直到整个数组有序。时间复杂度为 O(n^2)。
  3. 插入排序(Insertion Sort):将待排序的元素插入到已排序的部分中的正确位置,重复这个过程直到整个数组有序。时间复杂度为 O(n^2)。
  4. 快速排序(Quick Sort):选择一个基准元素,将数组划分为两个部分,一部分小于基准元素,一部分大于基准元素,对这两个部分递归进行快速排序,最终得到有序数组。时间复杂度为 O(nlogn)。
  5. 归并排序(Merge Sort):将数组分为两个部分,对这两个部分递归进行归并排序,最后将这两个有序部分合并成一个有序数组。时间复杂度为 O(nlogn)。
  6. 堆排序(Heap Sort):将数组构建成一个最大堆,取出堆顶元素放到数组末尾,重复这个过程直到整个数组有序。时间复杂度为 O(nlogn)。
  7. 计数排序(Counting Sort):统计每个元素出现的次数,然后根据元素的大小将它们放到正确的位置上,最终得到有序数组。时间复杂度为 O(n+k),其中 k 表示元素的范围。
  8. 桶排序(Bucket Sort):将元素分到不同的桶中,对每个桶中的元素进行排序,最后将所有桶中的元素合并成一个有序数组。时间复杂度为 O(n+k),其中 k 表示桶的数量。
  9. 基数排序(Radix Sort):将元素按照位数进行排序,从最低位到最高位依次进行排序,最终得到有序数组。时间复杂度为 O(d*(n+k)),其中 d 表示元素的位数,k 表示元素的范围。

以下是Java语言实现的一些常见排序算法的代码示例:

  1. 冒泡排序
public static void bubbleSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

冒泡排序的实现思路是通过比较相邻的两个元素的大小,将较大的元素向后移动,最终将最大的元素移到数组末尾,重复这个过程直到整个数组有序。具体实现时,我们需要使用两个嵌套的循环,外层循环控制排序的轮数,内层循环控制每一轮比较的次数。

代码中的 bubbleSort 方法接收一个整型数组 arr 作为参数,使用变量 n 记录数组的长度。外层循环从数组的第一个元素开始,到倒数第二个元素结束,内层循环从数组的第一个元素开始,到 n-i-1 结束。在内层循环中,如果相邻的两个元素的大小关系不符合要求,就将它们交换位置。这样每一轮内层循环结束后,最大的元素就会被移到数组的末尾。

  1. 选择排序
public static void selectionSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        int temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
}

选择排序的实现思路是从未排序的部分中选择最小的元素放到已排序的部分的末尾,重复这个过程直到整个数组有序。具体实现时,我们需要使用两个嵌套的循环,外层循环控制排序的轮数,内层循环控制每一轮比较的次数。

代码中的 selectionSort 方法接收一个整型数组 arr 作为参数,使用变量 n 记录数组的长度。外层循环从数组的第一个元素开始,到倒数第二个元素结束,内层循环从外层循环的下一个元素开始,到数组的最后一个元素结束。在内层循环中,找到未排序部分中最小的元素,并记录其下标。最后将最小元素和未排序部分的第一个元素交换位置,这样每一轮内层循环结束后,未排序部分的最小元素就会被移到已排序部分的末尾。

  1. 插入排序
public static void insertionSort(int[] arr) {
    int n = arr.length;
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

插入排序的实现思路是将待排序的元素插入到已排序的部分中的正确位置,重复这个过程直到整个数组有序。具体实现时,我们需要使用一个循环,从第二个元素开始,将当前元素插入到已排序部分的正确位置。

代码中的 insertionSort 方法接收一个整型数组 arr 作为参数,使用变量 n 记录数组的长度。外层循环从数组的第二个元素开始,到最后一个元素结束。内层循环从当前元素的前一个元素开始,到已排序部分的第一个元素结束。在内层循环中,如果已排序部分的当前元素大于当前元素,就将已排序部分的当前元素向后移动一位。最后将当前元素插入到已排序部分的正确位置。

  1. 快速排序
public static void quickSort(int[] arr, int left, int right) {
    if (left < right) {
        int pivotIndex = partition(arr, left, right);
        quickSort(arr, left, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, right);
    }
}

private static int partition(int[] arr, int left, int right) {
    int pivot = arr[right];
    int i = left - 1;
    for (int j = left; j < right; j++) {
        if (arr[j] < pivot) {
            i++;
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    int temp = arr[i + 1];
    arr[i + 1] = arr[right];
    arr[right] = temp;
    return i + 1;
}

快速排序的实现思路是选择一个基准元素,将数组划分为两个部分,一部分小于基准元素,一部分大于基准元素,对这两个部分递归进行快速排序,最终得到有序数组。具体实现时,我们需要使用递归的方式,不断将数组划分为两个部分,并对这两个部分进行快速排序。

代码中的 quickSort 方法接收一个整型数组 arr、数组的左边界 left 和右边界 right 作为参数。在方法中,我们首先判断左边界是否小于右边界,如果小于,则选取数组的第一个元素作为基准元素,将数组划分为两个部分,一部分小于基准元素,一部分大于基准元素。然后对这两个部分分别递归调用 quickSort 方法,直到每个部分只剩下一个元素或没有元素为止。

  1. 归并排序
public static void mergeSort(int[] arr, int left, int right) {
    if (left < right) {
        int mid = (left + right) / 2;
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        merge(arr, left, mid, right);
    }
}

private static void merge(int[] arr, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;
    int[] L = new int[n1];
    int[] R = new int[n2];
    for (int i = 0; i < n1; i++) {
        L[i] = arr[left + i];
    }
    for (int j = 0; j < n2; j++) {
        R[j] = arr[mid + 1 + j];
    }
    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

归并排序的实现思路是将数组不断划分为两个部分,对每个部分进行排序,然后将两个有序部分合并为一个有序数组。具体实现时,我们需要使用递归的方式,不断将数组划分为两个部分,并对这两个部分进行排序和合并。

代码中的 mergeSort 方法接收一个整型数组 arr 作为参数。在方法中,我们首先判断数组的长度是否大于1,如果大于1,则将数组划分为两个部分,分别递归调用 mergeSort 方法。然后将两个有序部分合并为一个有序数组。

  1. 堆排序
public static void heapSort(int[] arr) {
    int n = arr.length;
    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }
    for (int i = n - 1; i >= 0; i--) {
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;
        heapify(arr, i, 0);
    }
}

private static void heapify(int[] arr, int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }
    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }
    if (largest != i) {
        int temp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = temp;
        heapify(arr, n, largest);
    }
}

堆排序的实现思路是将数组看作是一个完全二叉树,将其转换为一个最大堆,然后将堆顶元素与最后一个元素交换,再将剩余元素重新构建为一个最大堆,重复这个过程直到整个数组有序。具体实现时,我们需要使用一个循环,将数组构建为一个最大堆,然后将堆顶元素与最后一个元素交换,再将剩余元素重新构建为一个最大堆。

代码中的 heapSort 方法接收一个整型数组 arr 作为参数。在方法中,我们首先将数组构建为一个最大堆,然后将堆顶元素与最后一个元素交换,再将剩余元素重新构建为一个最大堆,重复这个过程直到整个数组有序。

  1. 计数排序
public static void countingSort(int[] arr) {
    int n = arr.length;
    int max = arr[0], min = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
        if (arr[i] < min) {
            min = arr[i];
        }
    }
    int range = max - min + 1;
    int[] count = new int[range];
    for (int i = 0; i < n; i++) {
        count[arr[i] - min]++;
    }
    for (int i = 1; i < range; i++) {
        count[i] += count[i - 1];
    }
    int[] output = new int[n];
    for (int i = n - 1; i >= 0; i--) {
        output[count[arr[i] - min] - 1] = arr[i];
        count[arr[i] - min]--;
    }
    for (int i = 0; i < n; i++) {
        arr[i] = output[i];
    }
}

计数排序的实现思路是统计数组中每个元素出现的次数,然后根据元素出现的次数将数组重新排序。具体实现时,我们需要使用两个数组,一个用于统计每个元素出现的次数,另一个用于存放排序后的结果。

代码中的 countingSort 方法接收一个整型数组 arr 作为参数。在方法中,我们首先找到数组中的最

  1. 桶排序
public static void bucketSort(int[] arr, int bucketSize) {
    if (arr.length == 0) {
        return;
    }
    int max = arr[0], min = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
        if (arr[i] < min) {
            min = arr[i];
        }
    }
    int bucketCount = (max - min) / bucketSize + 1;
    List<List<Integer>> buckets = new ArrayList<>(bucketCount);
    for (int i = 0; i < bucketCount; i++) {
        buckets.add(new ArrayList<>());
    }
    for (int i = 0; i < arr.length; i++) {
        int bucketIndex = (arr[i] - min) / bucketSize;
        buckets.get(bucketIndex).add(arr[i]);
    }
    int k = 0;
    for (int i = 0; i < bucketCount; i++) {
        List<Integer> bucket = buckets.get(i);
        Collections.sort(bucket);
        for (int j = 0; j < bucket.size(); j++) {
            arr[k] = bucket.get(j);
            k++;
        }
    }
}

桶排序的实现思路是将数组中的元素分配到若干个桶中,对每个桶中的元素进行排序,然后依次将每个桶中的元素输出,最终得到有序数组。具体实现时,我们需要使用一个循环,将每个元素分配到对应的桶中,对每个桶中的元素进行排序,然后依次将每个桶中的元素输出。

代码中的 bucketSort 方法接收一个整型数组 arr 和桶的数量 bucketNum 作为参数。在方法中,我们首先确定每个桶的范围,然后将每个元素分配到对应的桶中,对每个桶中的元素进行排序,最后将每个桶中的元素依次输出。

  1. 基数排序
public static void radixSort(int[] arr) {
    int max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    for (int exp = 1; max / exp > 0; exp *= 10) {
        countingSortByDigit(arr, exp);
    }
}

private static void countingSortByDigit(int[] arr, int exp) {
    int n = arr.length;
    int[] output = new int[n];
    int[] count = new int[10];
    for (int i = 0; i < n; i++) {
        count[(arr[i] / exp) % 10]++;
    }
    for (int i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }
    for (int i = n - 1; i >= 0; i--) {
        output[count[(arr[i] / exp) % 10] - 1] = arr[i];
        count[(arr[i] / exp) % 10]--;
    }
    for (int i = 0; i < n; i++) {
        arr[i] = output[i];
    }
}

基数排序的实现思路是将整数按照位数进行排序,具体实现时,我们需要使用一个循环,将整数按照位数进行排序,重复这个过程直到最高位,最终得到有序数组。具体实现时,我们需要使用一个循环,将整数按照位数进行排序,重复这个过程直到最高位,最终得到有序数组。

代码中的 radixSort 方法接收一个整型数组 arr 作为参数。在方法中,我们首先确定数组中最大元素的位数,然后按照位数进行排序,重复这个过程直到最高位,最终得到有序数组。

10.希尔排序

它是插入排序的一种变体,也被称为缩小增量排序。希尔排序的基本思想是将待排序的数组分割成若干个子序列,分别进行插入排序,然后逐步缩小子序列的长度,最终完成整个数组的排序。希尔排序的时间复杂度为 O(nlogn) 或 O(n^2),具体取决于所选取的增量序列。

以下是希尔排序的 Java 代码实现:

public static void shellSort(int[] arr) {
    int len = arr.length;
    int gap = len / 2;
    while (gap > 0) {
        for (int i = gap; i < len; i++) {
            int temp = arr[i];
            int j = i;
            while (j >= gap && arr[j - gap] > temp) {
                arr[j] = arr[j - gap];
                j -= gap;
            }
            arr[j] = temp;
        }
        gap /= 2;
    }
}

希尔排序的基本思想是将待排序的数组分割成若干个子序列,分别进行插入排序,然后逐步缩小子序列的长度,最终完成整个数组的排序。具体来说,希尔排序的实现过程如下:

  1. 首先,将数组的长度除以 2,得到一个增量 gap。
  2. 将数组分成 gap 个子序列,分别对每个子序列进行插入排序。
  3. 缩小增量 gap,重复步骤 2,直至增量为 1。
  4. 对整个数组进行插入排序。

在代码实现中,我们首先获取数组的长度 len,并将增量 gap 初始化为 len 的一半。然后,我们进入一个 while 循环,该循环会不断缩小增量 gap,直至增量为 1。在每次循环中,我们对每个子序列进行插入排序。具体来说,我们从第 gap 个元素开始遍历数组,将当前元素存储到临时变量 temp 中,并从后往前遍历子序列,将大于 temp 的元素向后移动 gap 个位置,直至找到一个小于或等于 temp 的元素。然后,我们将 temp 插入到该元素的后面,完成一次插入排序。最后,我们缩小增量 gap 并重复上述步骤,直至完成整个数组的排序。

希尔排序的时间复杂度取决于所选取的增量序列。在最坏情况下,希尔排序的时间复杂度为 O(n^2),但在一般情况下,它的时间复杂度为 O(nlogn)。

8. 遍历算法

  • 前序遍历:先访问根节点,再访问左子树,最后访问右子树。
public static void preorderTraversal(TreeNode root) {
    if (root != null) {
        System.out.print(root.val + " ");
        preorderTraversal(root.left);
        preorderTraversal(root.right);
    }
}

以下是前序遍历的示意图:

        1
       / \
      2   3
     / \
    4   5

前序遍历结果为:1 2 4 5 3

  • 中序遍历:先访问左子树,再访问根节点,最后访问右子树。
public static void inorderTraversal(TreeNode root) {
    if (root != null) {
        inorderTraversal(root.left);
        System.out.print(root.val + " ");
        inorderTraversal(root.right);
    }
}

以下是中序遍历的示意图:

        1
       / \
      2   3
     / \
    4   5

中序遍历结果为:4 2 5 1 3

  • 后序遍历:先访问左子树,再访问右子树,最后访问根节点。
public static void postorderTraversal(TreeNode root) {
    if (root != null) {
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        System.out.print(root.val + " ");
    }
}

以下是后序遍历的示意图:

        1
       / \
      2   3
     / \
    4   5

后序遍历结果为:4 5 2 3 1

9. 查找算法:

  1. 线性查找

线性查找(Linear Search)是一种最简单、最基础的查找算法,它逐个比较数组中的元素,直到找到目标元素为止。线性查找的时间复杂度为 O(n),其中 n 是数组的长度。

以下是线性查找的 Java 代码实现:

public static int linearSearch(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}
  1. 二分查找

二分查找(Binary Search)是一种基于比较目标值和数组中间元素的查找算法。在二分查找中,我们首先将数组按升序排列,然后将目标值与数组中间的元素进行比较。如果目标值等于中间元素,则查找成功并返回该元素的下标;如果目标值小于中间元素,则在左半部分继续查找;如果目标值大于中间元素,则在右半部分继续查找。重复以上步骤直至查找成功或者查找失败。

二分查找的时间复杂度为 O(logn),其中 n 是数组的长度。

以下是二分查找的 Java 代码实现:

public static int binarySearch(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}
  1. 插值查找

插值查找(Interpolation Search)是一种基于目标值在数组中的位置估计来进行查找的算法。在插值查找中,我们首先将数组按升序排列,然后根据目标值在数组中的位置估计出它在数组中的大致位置,然后从该位置开始向左或向右查找。具体来说,设目标值为 target,数组的左边界为 left,右边界为 right,当前查找位置为 pos,则 pos 的计算公式为:

pos = left + (target - arr[left]) * (right - left) / (arr[right] - arr[left])

在计算出 pos 后,我们将目标值与 arr[pos] 进行比较。如果目标值等于 arr[pos],则查找成功并返回 pos;如果目标值小于 arr[pos],则在左半部分继续查找;如果目标值大于 arr[pos],则在右半部分继续查找。重复以上步骤直至查找成功或者查找失败。

插值查找的时间复杂度为 O(loglogn),其中 n 是数组的长度。当数组中元素分布比较均匀时,插值查找的效率比二分查找高。

以下是插值查找的 Java 代码实现:

public static int interpolationSearch(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    while (left <= right && target >= arr[left] && target <= arr[right]) {
        int pos = left + (target - arr[left]) * (right - left) / (arr[right] - arr[left]);
        if (arr[pos] == target) {
            return pos;
        } else if (arr[pos] < target) {
            left = pos + 1;
        } else {
            right = pos - 1;
        }
    }
    return -1;
}
  1. 斐波那契查找

斐波那契查找(Fibonacci Search)是一种基于斐波那契数列的查找算法。在斐波那契查找中,我们首先将数组按升序排列,然后根据斐波那契数列中的数值计算出一个数值 k,使得数组长度为 F(k)-1(其中 F(k) 表示第 k 个斐波那契数)。然后,我们从数组的左边界和 F(k-2)-1 的位置开始查找,根据目标值与 arr[mid] 的大小关系,将查找范围缩小至左半部分或右半部分。重复以上步骤直至查找成功或者查找失败。

斐波那契查找的时间复杂度为 O(logn),其中 n 是数组的长度。

以下是斐波那契查找的 Java 代码实现:

public static int fibonacciSearch(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    int k = 0; // 斐波那契数列的下标
    int[] fib = generateFibonacci(); // 生成斐波那契数列
    while (right > fib[k] - 1) {
        k++;
    }
    int[] temp = Arrays.copyOf(arr, fib[k]); // 将数组扩展到长度为 F(k)-1
    for (int i = right + 1; i < fib[k]; i++) {
        temp[i] = arr[right]; // 将扩展的位置赋值为数组的最后一个元素
    }
    while (left <= right) {
        int mid = left + fib[k-1] - 1;
        if (target < temp[mid]) {
            right = mid - 1;
            k--;
        } else if (target > temp[mid]) {
            left = mid + 1;
            k -= 2;
        } else {
            if (mid <= right) {
                return mid;
            } else {
                return right;
            }
        }
    }
    return -1;
}

private static int[] generateFibonacci() {
    int[] fib = new int[50];
    fib[0] = 1;
    fib[1] = 1;
    for (int i = 2; i < 50; i++) {
        fib[i] = fib[i-1] + fib[i-2];
    }
    return fib;
}
  1. 哈希查找

哈希查找(Hash Search)是一种基于哈希表的查找算法。在哈希查找中,我们首先将数组中的元素通过哈希函数映射到哈希表中的位置,然后在哈希表中查找目标元素。如果哈希表中存在该元素,则查找成功并返回该元素的下标;否则查找失败。

哈希查找的时间复杂度为 O(1),但是需要消耗额外的空间来存储哈希表。

以下是哈希查找的 Java 代码实现:

public static int hashSearch(int[] arr, int target) {
    int[] hashTable = new int[10000]; // 哈希表,大小为 10000
    Arrays.fill(hashTable, -1); // 初始化为 -1
    for (int i = 0; i < arr.length; i++) {
        int hash = arr[i] % 10000; // 哈希函数,取模运算
        while (hashTable[hash] != -1) { // 处理哈希冲突
            hash = (hash + 1) % 10000;
        }
        hashTable[hash] = i; // 将元素插入哈希表
    }
    int hash = target % 10000; // 哈希函数,取模运算
    while (hashTable[hash] != -1) { // 处理哈希冲突
        if (arr[hashTable[hash]] == target) {
            return hashTable[hash];
        }
        hash = (hash + 1) % 10000;
    }
    return -1;