数据结构与算法

1.时间空间复杂度

注:(时间空间复杂度=时间和空间的增长趋势)

1.1时间复杂度(BigO)

【大O表示法】算法的渐进时间复杂度-> T(n)=O(f(n))

注:T(n)代表的是算法的渐进时间复杂度,f(n)是代码的执行次数

  1. for(int i =1 ;i<n;i++){ /*此行代码执行1+2N次*/
  2. x++; /*此行代码执行N次*/
  3. } /*而在BigO中n是看成无穷的,所以此时有O(1+3N)=O(N)*/
  4. for(int i =1;i<n;i++){
  5. for(int j =1;i<n;j++){
  6. x++; /*同理,两个循环的代码的O(N)=O(N^2)*/
  7. }
  8. } /*此时,则有O(N)=O(N^2+N)*/
  1. int x=0;
  2. int y=1;
  3. int temp =x;
  4. x=y;
  5. y=temp; /*该代码块的O(N)=O(1)*/
  1. int i =1;
  2. while(i<n){ /*假设i在k次方时等于n,即2^k=n,所以k=logN*/
  3. i=i*2; /*所以该代码块的O(N)=O(logN)*/
  4. }
  1. for(int i=1;i<n;i++){
  2. int x=1; /*由代码块1和代码块3得O(N)=O(NlogN)*/
  3. while(x<n){
  4. x=x*2;
  5. }
  6. }

1.2空间复杂度

内存空间增长的趋势

  1. int x=0;
  2. int y=0;
  3. x++;
  4. y++; /*空间复杂度为O(1)*/
  1. int[] newArray = new int[n];
  2. for(int i=0;i<n;i++){
  3. newArray[i]=i; /*空间复杂度O(n)*/
  4. }

2.排序算法

2.1插入排序(Insertion Sort)

在插入排序中,我们从前到后依次处理未排好序的元素,对于每个元素,我们将它与之前排好序的元素进行比较,找到对应的位置后并插入。

  1. public void insertionSort(int[] array){
  2. for(int i=1;i<array.lenth;i++){ //从这个数组的第二个元素开始,从后向前扫描
  3. int cur=array[i]; //当前扫描的赋给cur
  4. int insertionIndex=i-1;
  5. while(insertionIndex>=0&&array[insertionIndex]>cur){ //如果上一个数比当前的数大
  6. array[insertionIndex+1]=array[insertionIndex]; //则两个数调换位置
  7. insertionIndex--; //--向前面的数继续进行判断
  8. }
  9. array[insertionIndex+1]=cur; //若while的代码进行执行则两个数调换位置,没有执行的话,被扫描到的这个数位置不变
  10. }
  11. }

注:这段代码的时间复杂度为O(n^2),空间复杂度是O(1)。

2.2快速排序(Quick Sort)

快排是一种分治(Divide and Conquer)算法,在这个算法中,我们吧大问题变成小问题,然后将小问题逐个解决,当小问题解决玩时,大问题也迎刃而解了。

  1. public void quickSort(int[] array,int left,int right){
  2. if(left>=right)return;
  3. int partitionIndex=partition(array,left,right);
  4. quickSort(array,partitionIndex+1,right);
  5. }
  6. public int partition(int[] array,int left,int right){
  7. int pivot=array[right];
  8. int leftIndex=left;
  9. int rightIndex=right-1;
  10. while(true){
  11. while(lefiIndex<right&array[leftIndex]<=pivot){
  12. leftIndex++;
  13. }
  14. while(rightIndex>=left&&array[rightIndex]>pivot){
  15. rightIndex--;
  16. }
  17. if(leftIndex>rightIndex)break;
  18. swap(array,leftIndex,rightIndex);
  19. }
  20. swap(array,leftIndex,right)
  21. return leftIndex;
  22. }
  23. public void swap(int[] array,int left,int right){
  24. int temp=array[left];
  25. array[left]=array[right];
  26. array[right]=temp;
  27. }

2.3归并排序(Merge Sort)

在此算法中,我们将一个数组氛围两个子数组,通过递归重复将数组切分到只剩下一个元素为止,然后把每个子数组中的元素排序后合并,通过不断合并子数组,最后就会拿到一个排序好的大数组。

  1. public void mergeSort(int[] array){
  2. int[] helper=copy(array);
  3. mergeSort(array,helper,0,array.length-1);
  4. return array;
  5. }
  6. public void mergeSort(int[] array,int[] helper,int left,int right){
  7. if(right-left<1)return;
  8. int mid=left+(right-left)/2;
  9. mergeSort(array,helper,left,mid);
  10. mergeSort(array,helper,mid+1,right);
  11. merge(array,helper,left,mid,right);
  12. }
  13. public void merge(int[] array,int[] helper,int left,int mid,int right){
  14. for(int i =left;i<=right;i++){
  15. if(leftStart>mid){
  16. array[i]=helper[rightStart++];
  17. }else if(rightStart>right){
  18. array[i]=helper[leftStart++];
  19. }else if(helper[leftStart]<helper[rightStart]){
  20. array[i]=helper[leftStart++];
  21. }else{
  22. array[i]=helper[rightStart++];
  23. }
  24. }
  25. }

3.链表

3.1链表包括

单链表、双链表、循环链表

数据结构与算法 - 图1

3.2链表的基本操作:

  1. 插入:将一个新元素插入链表的任意位置
  2. 删除:将一个元素从链表中删除
  3. 查找(遍历):查找一个特定的元素
  4. 更新:更新一个特定节点上的元素
/*链表的定义*/
public class LinkedList{
    static class ListNode(    //节点的定义ListNode
        int val;            //节点上面的支
        ListNode next;)        //指针,指向下一个节点
        public ListNode(int val){
            this.val=val;
        }

        ListNode head;        //头指针
        ListNode tail;        //尾指针
        int size;            //记录当前的节点数量
    public LinkedList(){
        head=null;
        tail=null;
        size=0;
    }
}
/*插入    注:需要考虑是从这个链表的头部、中间还是尾部进行节点的插入*/
public void insert(int position,int number){
    if(position>size){
        return;
    }
    ListNode newNode=new ListNode(number);
    if(position==0){
        newNode.next=head;
        head=newNode;
        if(tail==null){
            tail=newNode;
        }
        size++;
    }else if(position==size){
        this.append(number);
    }else{
        ListNode prev=head;
        for(int i=0;i<position-1;i++){
            prev=prev.next;
        }
        ListNode next=prev.next;
        newNode.next=next;
        prev.next=newNode;
        size++
    }
}

public void append(int number){
    ListNode newNode=new ListNode(number);
    if(tail==null){
        tail=newNode;
    }else{
        tail.next=newNode;
        tail=newNode
    }
    size++;
}
/*删除*/
public void delete(int number){
    if(head!=null&&head.val==number){
        head=head.next;
        size--;
        if(size==0){
            tail=head;
        }
    }else{
        ListNode prev=head;
        ListNode cur=head;
        while(prev!=null&&cur!=null){
            if(cur.val==number){
                if(cur==tail){
                    tail=prev;
                }
                prev.next=cur.next;
                size--;
                return;
            }
            prev=cur;
            cur=cur.next;
        }
    }
}
/*查找*/
public int search(int number){
    ListNode cur=head;
    for(int index=0;cur!=null;index++){
        if(cur.val==number){
            return index;
        }
        cur=cur.next;
    }
    return -1;
}
/*更新*/
public int update(int oldValue,int newValue){
    ListNode cur=head;
    for(int index=0;cur!=null;index++){
        if(cur.val==oldValue){
             cur.val=newValue;
             return index;
        }
        cur=cur.next;
    }
       retrun -1;
}

4.栈和队列

4.1堆栈(Stack)

堆栈是一种常用的数据结构,这种数据结构的存储方式和垃圾桶一样,后面放进去的元素可以先取出来,而最早放入的元素会被压在最下面,最后才能被拿出来。我们也可以把栈的储存方式简单理解为堆盘子,后面加入的盘子会被堆到最上面,最早堆入的盘子在最下面。

所以栈是一种后进先出(Last In First Out)的数据结构,后入的元素先出,先入的元素后出。

堆栈主要支持以下的两种操作:

  • 入栈(Push):将一个元素放入栈,用来加入数据。
  • 出栈(Pop):将一个元素弹出栈,用来删除数据。

还有以下两种辅助操作:

  • Peek:查看最顶部的元素。
  • isEmpty:查看栈是否为空。
/*数组栈的定义(即,用数组实现栈)*/
public class Stack {        
    static final int CAPACITY = 1000;        //CAPACITY用来限制栈的容量
    int top;                                //使用指针top来记录顶端元素的位置
    int stack[];

    public Stack() {
        top = -1;                            //top初始化-1,代表数组栈没有任何元素
        stack = new int[CAPACITY];
    }
}
/*入栈(push)*/
public boolean push(int val) {
    if (top >= (CAPACITY - 1)) {    //在push中,需要判断顶部元素是否达到容量限制
        System.out.println("Stack Overflow.");
        return false;                //如果达到容量限制,则输出“溢出栈”错误
    }
    stack[++top] = val;                //否则,移动top指针,加入新的元素
    return true;
}

/*出栈(pop)*/
public int pop() {
    if (top < 0) {                    //在pop中,需要判断栈是否为空
        System.out.println("Stack Underflow.");
        return 0;                    //如果栈为空,则输出“栈下溢”错误
    }
    int element = stack[top--];        //否则将top指针减一,进行出栈操作
    return element;
}
/*查看最顶部的元素(peek)*/
public int peek() {
    if (top < 0) {                //需要判断栈是否为空
        System.out.println("Stack Underflow");
        return 0;
    }
    int element = stack[top];
    return element;
}

/*查看栈是否为空(isEmpty)*/
public boolean isEmpty() {
    return top < 0;
}

4.2队列(Queue)

队列是很好理解的一种数据结构,顾名思义,队列数据结构就和我们平时排队一样,先进入的元素先出,后进入的元素后出。队列的两端都是开的,一段负责插入新元素,另一端负责删除元素。

队列主要支持以下两种操作:

  • 入队(enqueue):增加一个新的元素
  • 出队(dequeue):删除一个元素

还支持其他辅助操作:

  • peek – 查看队列最前端的元素
  • isFull – 查看队列是否满了
  • isEmpty – 查看队列是否为空
/*循环队列,它和基础的顺序队列相比较,更能有效地利用数组空间*/
public class ArrayQueue {
    int front, rear, size;        //两个指针front、rear,front来指向队列的头部,rear指向队列尾部,size来记录元素的数量
    int capacity;                //capacity来限制队列的长度
    int array[];

    public ArrayQueue(int capacity) {
        this.capacity = capacity;
        front = rear = size = 0;
        array = new int[capacity];
    }
}
/*入队——增加一个新元素*/
public void enqueue(int item) {
    if (isFull()) return;
    array[rear] = item;
    rear = (rear + 1) % capacity;    //防止rear超过队列的内存
    size++;
    System.out.println(item + " is enqueued.");
}

/*出队——删除一个元素*/
public int dequeue() {
    if (isEmpty()) return Integer.MIN_VALUE;
    int item = array[front];
    front = (front + 1) % capacity;
    size --;
    return item;
}

/*查看队列最前端的元素*/
public int peek() {
    if (isEmpty()) return Integer.MIN_VALUE;
    return array[front];
}

/*查看队列是否满了*/
public boolean isFull() {
    return size == capacity;
}

/*查看队列是否为空*/
public boolean isEmpty() {
    return size == 0;
}

5.优先队列和哈希表

5.1哈希表的介绍

  • 数组:寻址容易,插入和删除元素困难
  • 链表:寻址困难,插入和删除元素容易

而哈希表则是数组和链表的结合

数据结构与算法 - 图2

5.2哈希表支持的操作:

  • get(K key):通过特定的关键字拿到其所对应的值
  • add(Key key, Value value):将一对新的键值对加入哈希表
  • remove(Key key):通过关键字,删除哈希表中的键值对
  • getSize():当前键值对的数量
  • isEmpty():查看哈希表是否为空
/*哈希表的实现*/
import java.util.ArrayList;

public class HashMap {
    static class HashNode<String, Integer> {    //HashNode是键值对节点的定义
        String key;            //关键字
        Integer value;        //数值
        HashNode<String, Integer> next;            //next指向下一个节点
        public HashNode(String key, Integer value) {
            this.key = key;
            this.value = value;
        }
    }

    private ArrayList<HashNode<String, Integer>> bucketArray;
    private int numBuckets;
    private int size;

    public HashMap() {                    //HashMap是哈希表的定义
        bucketArray = new ArrayList<>();//ArrayLis的bucketArray,可理解为哈希表图左侧的数组
        numBuckets = 10;                //numBuckets代表哈希表数组的长度
        size = 0;                        //size代表当前bucket的数量
        for(int i = 0; i < numBuckets; i++) {    //在每个buckArray上加一个空的头节点
            bucketArray.add(null);
        }
    }
}
/*输入一个关键字,输出是关键字对应的bucket位置*/
private int getBucketIndex(String key) {    
    int hashCode = key.hashCode();
    int index = hashCode % numBuckets;
    return index;
}
/*将一对新的键值加入哈希表*/
public void add(String key, Integer value) {
    int bucketIndex = getBucketIndex(key);//使用getBucketIndex拿到关键字对应的bucket指数
    HashNode<String, Integer> head = bucketArray.get(bucketIndex);
    while (head != null) {
        if (head.key.equals(key)) {    //寻找与关键词对应的节点
            head.value = value;    //找到则更新节点数据
            return;
        }
        head = head.next;        //没找到则创建一个新的键值对节点,再把新节点更新为对应bucket的头节点
    }
    size++;
    head = bucketArray.get(bucketIndex);
    HashNode<String, Integer> newNode = new HashNode<String, Integer>(key, value);
    newNode.next = head;
    bucketArray.set(bucketIndex, newNode);
    if ((1.0 * size) / numBuckets >= 0.7) {//如果当前键值对数量超过了bucket容量的70%
        ArrayList<HashNode<String, Integer>> temp = bucketArray;
        bucketArray = new ArrayList<>();    //则创建一个长度为当前数量两倍的bucketArray,并把当前的节点转移到新的bucketArray上
        numBuckets = 2 * numBuckets;
        size = 0;
        for (int i = 0; i < numBuckets; i++) {
            bucketArray.add(null);
        }
        for (HashNode<String, Integer> headNode : temp) {
            while(headNode != null) {
                add(headNode.key, headNode.value);
                headNode = headNode.next;
            }
        }
    }
}
/*get函数是通过关键字找到对应数组的函数*/
public Integer get(String key) {
    int bucketIndex = getBucketIndex(key);    //先通过getBucketIndex拿到关键字对应的bucket指数
    HashNode<String, Integer> head = bucketArray.get(bucketIndex);
    while (head != null) {        //并拿到head头指针,顺着对应的链表往下走
        if (head.key.equals(key)) {    //寻找是否有对应的关键字的节点
            return head.value;        //找到则返回对应的数值
        }
        head = head.next;
    }
    return null;                    //未找到则返回null
}
/*通过关键字,删除哈希表中的键值对*/
public Integer remove(String key) {
    int bucketIndex = getBucketIndex(key);//先通过getBucketIndex拿到关键字对应的bucket指数
    HashNode<String, Integer> head = bucketArray.get(bucketIndex);
    HashNode<String, Integer> prev = null;    //拿到头指针,通过迭代next指针
    while (head != null) {                    //使用删除链表节点的方式来删除对应的节点
        if (head.key.equals(key))
            break;
        prev = head;
        head = head.next;
    }
    if (head == null) {
        return null;
    }
    size--;
    if (prev != null) {
        prev.next = head.next;
    } else {
        bucketArray.set(bucketIndex, head.next);
    }
    return head.value;
}
/*查看当前键值对的数量*/
public int size() {
    return size;
}

/*查看哈希表是否为空*/
public boolean isEmpty() {
    return size() == 0;
}

6.树

6.1树的种类

  • 二叉树(Binary Tree):每个节点最多含有两个子节点,上面图示中的树就是二叉树。
  • 完全二叉树(Complete Binary Tree):假设一个二叉树深度(depth)为d(d > 1),除了第d层外,其它各层的节点数量均已达到最大值,且第d层所有节点从左向右紧密排列,这样的二叉树就是完全二叉树。
  • 满二叉树(Full Binary Tee):在满二叉树中,每个不是尾节点的节点都有两个子节点。

数据结构与算法 - 图3

  • 排序二叉树(Binary Search Tree):在此树中,每个节点的数值比左子树上的每个节点都大,比所有右子树上的节点都小。
  • 平衡二叉树(AVL Tree):任何节点的两颗子树的高度差不大于1的二叉树。

注:AVL Tree图中左支树的高度为二,右支树的高度为一

数据结构与算法 - 图4

  • B树(B-Tree):B树和平衡二插树一样,只不过它是一种多叉树(一个节点的子节点数量可以超过二)。

数据结构与算法 - 图5

  • 红黑树(Red—Black Tree):是一种自平衡二叉寻找树。

数据结构与算法 - 图6

6.2二分查找树(Binary Search Tree)的实现

/*二分查找树的实现*/
public class BST {

    static class TreeNode {
        public int value;        //记录数值
        public TreeNode left;    //left指向left child
        public TreeNode right;    //right指向right child
        public TreeNode(int value) {
            this.value = value;
        }
    }

    private TreeNode root;        //根节点root

}
/*在二分查找树中,查找某个数*/
public TreeNode get(int key){
    TreeNode current=root;        //从头节点开始搜索
    whlie(current!=null&&current.value!=key){        //判断条件,检测的值不为空,且未检测到目标值
        if(key<current.value){                        
            current=current.left;
        }else if(key>current.right){
            current=current.right;
        }
    }
    return current==null?null:current;
}
public void insert(int key) {
    if(root == null) {
        root = new TreeNode(key);
        return;
    }
    TreeNode current = root;        //current代表要插入的位置
    TreeNode parent = null;            //parent代表所插入的位置的父亲
    while(true) {
        parent = current;
        if(key < parent.value) {
            current = parent.left;
            if(current == null) {
                parent.left = new TreeNode(key);
                return;
            }
        } else if (key > parent.value){
            current = parent.right;
            if(current == null) {
                parent.right = new TreeNode(key);
                return;
            }
        } else {
            return; // BST does not allow nodes with the same value
        }
    }
}

6.3树的遍历

  • 前序遍历 A B D H E I C F J K G
  • 中序遍历 D H B E I A J F K C G
  • 后序遍历 H D I E B J F K G C A

数据结构与算法 - 图7

7.图(Graph)

7.1图的种类

  1. 无向图(Undirected Graph):在无向图中,每个顶点和其他顶点通过相连线连接。

  2. 有向图(Directed Graph):有向图中的相连线是有方向的。

  3. 权重图(Weighted Graph):在权重图中,每条相连线有各自的权重。
    有向图
    数据结构与算法 - 图8
    权重图

数据结构与算法 - 图9

7.2图的实现

7.2.1有向图的实现(Directed Graph)

有向图的实现有两种,一种是用矩阵(Matrix)的形式来实现,另一种是用链表(List)的形式来实现。

  • 使用矩阵实现有向图

数据结构与算法 - 图10

每行代表相应的顶点,如果M[i][j]= 1,那么就代表顶点 i 连向 j,如果是0,则表达顶点间没有联系。用矩阵的方式来实现图的优势很明显,我们可以很快地判断两个顶点之间是否相连,可是用矩阵实现的空间复杂度很高,我们需要O(V^2)来记录所有的数据

  • 使用链表实现有向图

数据结构与算法 - 图11

图的左侧用数组来实现,代表我们的所有顶点,而每个顶点含有一个链表,链表上储存了该顶点指向的顶点。优点是没有浪费空间

7.3图的遍历(Graph Traversal)

1.深度优先搜索(Depth-first Search)

注:在图的深度优先搜索中,我们尽可能先遍历一个顶点可以达到的最深处,其中可能会出现的问题就是会有循环出现,所以我们需要一个数组来记录哪些节点已经被访问过。

数据结构与算法 - 图12

  1. 访问A
  2. 访问B(在访问A之后,接下来应该访问的是A出发的另一个顶点,既顶点B)
  3. 访问C(在访问B之后,接下来访问的是从B出发的另一个顶点,既C,E,F。在此图中,我们按照字母排序顺序访问,因此先访问C。)
  4. 访问E(接下来访问与C连接的另一个顶点E。)
  5. 访问D(接下来访问从E出发的顶点B和D,因为B已被访问过,所以访问顶点D。)
  6. 访问F(接下来回溯“访问A的另一个连接顶点F”)
  7. 访问G

2.广度优先搜索(Breadth-first search)

广度优先搜索遍历图的过程是以v为起点,由近至远,依次访问和v有路径相通且路径长度为1,2,…的顶点。

数据结构与算法 - 图13

  1. 访问A
  2. 访问B
  3. 依次访问C,E,F(在B被访问之后,接下来访问B的邻接点,既C,E,F。)
  4. 依次访问D,G(在访问完C,E,F之后,再依次访问他们出发的另一个顶点。还是按照C,E,F的顺序访问,C的已经全部访问过了,那么就只剩下E,E;先访问E的邻接点D,再访问F的邻接点G。