链表结构,栈,队列,递归行为,哈希表和有序表

链表的结构与数组有着很大的不同,他不像数组一样,需要一块连续的地址空间,他可以通过指针将零散的空间连接起来,更加有效的提高了空间的利用率。

单链表:

线性表的链式存储就叫做单链表,下面来看看如何进行定义:

public class LinkedList{
    public class Node{
        public int data;//数据域
        public Node next;//存储下一个节点的地址    
    }
    private Node head = null;
}

说到单链表,我觉的还是有必要说一下他的一些特性,因为他不是一个连续的内存空间,所以做不到时间复杂度为O(1)的查找,单链表的插入,删除,的时间复杂度是O(1).比起数组来说要快一点。

//单链表的查找
public static Node find(int value){
    Node p = head;
    while(p != null && p.data != value){
        p = p.next;    
    }
    return p;
}
//单链表的插入
//再节点b,后面插入x
public static void(Node b,Node x){
    //没有b节点,只有head一个节点
    if(b == null){
        x.next = head;//将新加的节点指向为null
        head = x;//head节点存储新节点的地址    
    } else {
        x.next = b.next;
        b.next = x;            
    }
}
//单链表的删除
public static void remove(Node a,Node b){
    //在知道节点a的情况下删除节点b
    if(a == null){
        head = head.next;//如果a为头结点,那么a存储head.next,也就是null    
    } else {
        a.next = a.next.next;    
    }
}

下面再来看看双链表

大家应该从代码中看出来了,与单链表最大的不同就是增加了一个last指针,他的作用就是用来存储这个节点前一个结点的地址

public class DoubleNode{
    public int data;
    public DoubleNode last;
    public DoubleNode next;
 
    public DoubleNode(int data){
        this.data = data;    
    }
}

下面来看一个经典的题目,反转单链表和双链表

class ReverseList{
    //创建单链表结点
    public static class Node{
        public int data;
        public Node next;
        public Node(int data){
            this.data = data;        
        }
    }
    //双链表结点
    public static class DoubleNode{
        public int data;
        public DoubleNode last;
        public DoubleNode next;
        public DoubleNode(int data){
            this.data = data;        
        }                
    }
    //单链表反转
    public static Node reverseLinkedList(Node head){
        Node pre = null;
        Node next = null;
        while(head != null){
            //先用next存储head.next之后的结点数据
            next = head.next;
            //将head.next指向pre空节点
            head.next = pre;
            //用pre存储head结点的数据
            pre = head;
            //将next的数据重新给head
            head = next;        
        }
        //返回pre;    
        return pre;
    }
    //双链表反转
    public static Node reverseDoubleLinkedList(Node head){
        DoubleNode pre;
        DoubleNode next;
        while(head != null){
            next = head.next;
            head.next = pre;
            head.last = next;
            pre = head;
            head = next;        
        }    
        return pre;
    }
}

 再来看一个列题:删除链表中指定的值

public static Node removeValue(Node head,int value){
    //head来到第一个需要删除的位置
    while(head != null){
        if(head.value != value){
            break;        
        }    
        head = head.next;
    }
    Node pre = head;
    Node next = head;
    while(head != null){
        if(next.value == value){
            pre.next = next.next;        
        } else {
            pre = next;        
        }
        next = next.next;
    }
    return head;
}

下面我们来看看双端队列这个数据结构,双端队列理解起来很容易,就是两端开口的一种结构,可以实现头进头出,尾进尾出

public calss Node<T>{
    public T value;
    public Node<T> last;
    public Node<T> next;
    
    public Node<T>(T value){
        this.value = value;    
    }
}
//双端队列
public static class DoubleEndsQueue{
    //创建初始的头结点和尾结点
    public Node<T> head;
    public Node<T> tail;
    
    public void addFromHead(T value){
        Node<T> cur = new Node<>(value);
        if(head == null){
            head = cur;
            tail = cur;        
        } else {
            //从头进
            cur.next = head;
            head.last = cur;
            //重新定位头结点
            head = cur;        
        }
    }
    public void addFromTail(T value){
        Node<T> cur = new Node<T>(value);
        if(head == null){
            tail = cur;
            head = cur;        
        } else {
            tail.next = cur;
            cur.last = tail;
            //重新定位tail
            tail = cur;        
        }    
    }
    public T popFromHead(){
        //判断头节点是不是为空
        if(head == null){
            return null;        
        }
        //不为空定义指针节点
        Node cur = head;
        //判断头尾节点是否是一个
        if(head == tail){
            tail = null;
            head = null;        
        } else {
            head = head.next;
            cur.next = null;
            head.last = null;        
        }
        return cur.value;
    }
    public T popFromTail(){
        if(head == null){
            return null;        
        }    
        Node<T> cur = tail;
        if(head == tail){
            head = null;
            tail = null;        
        } else {
           //我们需要返回的节点是cur
           tail = tail.last;
           cur.last = null;
           tail.next = null;                               
        }
        return cur.value;
    }
    public boolean isEmpty(){
        return head = null;    
    }
}

 栈这个数据结构和双端队列有很大的相同的,不同的就是栈支持先进后出,一端开口

public static class MyStack<T>{
    private DoubleEndsQueue<T> stack;
    public MyStack<T>(){
        stack = new DoubleEndsQueue<T>();    
    }
    public void push(T value){
        stack.addFromHead(head);    
    }
    public T pop(){
        return stack.popFromHead();    
    }
    public boolean isEmpty(){
        return stack.isEmpty();    
    }
}

 队列也很简单,他的主要特性就是先进先出

public static class MyQueue<T>{
    DoubleEndsQueue<T> queue;
    public MyQueue<T>(){
        queue = new DoubleEndsQueue<T>();    
    }
    public void push(T value){
        queue.addFromHead(value);    
    }
    public T poll(){
        return queue.popFronTail();    
    }
    public boolean isEmpty(){
        return queue.isEmpty();    
    }
}

使用循环数组实现队列 是一个RingArray

面试官用一个不能够自由增长固定住的空间,你能不能完成这个功能,以此达到考察你Coding的目的

两个指针begin, end增加一个size变量, 解耦 begin跟 end谁管着我能不能加, 跟能不能拿: Size 管着只要Size没有到5,我必能加把7放到 end所在的位置,然后让end++如果用户拿东西, 能不能size要不等于 0, 必能拿, 拿begin位置的数, 然后begin++, size--

class RingArray{
    public static class MyQueue{
        private int[] arr;
        private int size;
        private int pushi;//尾进
        private int polli;//头出
        private finat int limit;            
        
        public MyQueue(int limit){
            arr = new int[limit];
            this.size = 0;
            this.pushi = 0;
            this.polli = 0;
            this.limit = limit;        
        }
        
        public int nextIndex(int i){
            return i < limit - 1 ? i+1 : 0;   //此处等于0的话可以将pushi再次等于0进行循环     
        }
        public void push(int value){
            //判断队列满了吗
            if(size == limit){
                throw new RuntimeException("队列满了");            
            }        
            size++;
            arr[pushi] = value;
            pushi = nextIndex(pushi);
        }
        public int pop(){
            if(size == 0){
               throw new RuntimeException("队列空了");                            
            }        
            size--;
            int ans = arr[polli];
            polli = nextIndex(polli);
            return ans;
        }
        public boolean isEmpty(){
            return size == 0;        
        }
    }
}

实现一个特殊的栈,在基本功能的基础上,再实现返回栈中最小元素的功能

1)pop、push、getMin操作的时间复杂度都是 O(1)。

2)设计的栈类型可以使用现成的栈结构。

class GetMinStack{
    public static class Mystack1{
        //准备两个栈,一个数据栈,一个最小栈
        private Stack<Integer> stackData;
        private Stack<Integer> stackMin;
        public Mystack1(){
            stackData = new Stack<>();
            stackMin = new Stack<>();        
        }                
        public void push(int newNum){
            if(this.stackMin.isEmpty()){
                this.stackMin.push(newNum);            
            } else if(newNum <= this.getMin()){
                this.stackMin.push(newNum);            
            }       
            this.stackData.push(newNum);
        }
        public int pop(){
            if(this.stackData.isEmpty()){
                throw new RuntimeException(" 空 ");            
            }      
            int value = this.stackData.pop();
            if(value == this.getMin()){
                this.stackMin.pop();            
            }
            return value;
        }
        public int getMin(){
            if(this.stackMin.isEmpty()){
                throw new RuntimeException("");            
            }        
            return this.stackMin.peek();
        }
    }
}

 用两个栈实现队列结构

class TwoStacksImplementQueue{
    public static class TwoStacksQueue{
        private Stack<Integer> stackPush;
        private Stack<Integer> stackPop;
        public TwoStacksQueue(){
            stackPush = new Stack<Integer>();
            stackPop = new Stack<Integer>();        
        }    
        //push栈向pop栈倒入
        public void pushToPop(){
            if(stackPop.isEmpty()){
                while(!stackPush.isEmpty()){
                    stackPop.push(stackPush.pop());                
                }            
            }        
        }
        public void int(int newInt){
            stackPush.push(newInt);
            pushToPop();        
        }
        public int poll(){
            if(stackPush.isEmpty() && stackPop.isEmpty()){
                throw new RuntimeException("");            
            }        
            pushToPop();
            return stackPop.pop();
        }
        public int peek(){
            if(stackPush.isEmpty() && stackPop.isEmpty()){
                throw new RuntimeException("");            
            }        
            pushToPop();
            return stackPop.peek();
        }
    }
}

两个队列来实现栈

class TwoQueueImplenceStack{
    public static class TwoQueueStack<T>{
        private Queue<T> queue;
        private Queue<T> help;
        public TwoQueueStack<T>(){
            queue = new LinkedList<T>();
            help = new LinkedList<T>();        
        }
         public void push(T value){
             queue.push(value);         
         }                     
         public T poll(){
             while(queue.size() > 1){
                 //留下队列queue中最后一个元素
                 help.offer(queue.poll());             
             }     
             T ans = queue.poll();
             Queue<T> temp = queue;
             queue = help;
             help = temp;
             return ans;    
         }  
         public T peek(){
           while(queue.size() > 1){
                 //留下队列queue中最后一个元素
                 help.offer(queue.poll());             
             }     
             T ans = queue.poll();
             help.offer(ans);
             Queue<T> temp = queue;
             queue = help;
             help = temp;
             return ans;                          
         }
         public boolean isEmpty(){
             return queue.isEmpty();         
         }
    }
}

}

关于递归,是个很玄学的东西,这个我们大家需要大量的练习总结经验才能会做

写递归:1. basecase, 递归终止条件

2. 处理当前层

3. 进入下一级递归

4. 现场恢复(非必须)

下面看一个经典的题目,用递归找出数组中的最大值

class GetMax{
    public static int getMax(int[] arr){
        process(arr,0,arr.length - 1);    
    }
    public static int process(int[] arr,int left,int right){
        if(left == right){
            return arr[left];        
        }
        int mid = left + ((right - left) >> 1);
        int leftMax = process(arr,left,mid);
        int rightMax = process(arr,mid + 1,right);
        return Math.max(leftMax,rightMax);
    }
}

递归树来了解递归过程。

 

哈希表和有序表

哈希函数的四个性质

1)输入是无限的,输出是有限的

2)相同输入得到相同输出

3)不同输入可能得到相同输出(哈希碰撞

4)我吗在进行输入的时候,一个输入通过f()函数对应一个输出,假设输入10000个,那么输出也有一万个,如果我们通过一个有范围的圈去在输出范围中圈出几个输出,那么圈出的数的个数基本相同。这其中是f()哈希函数的作用,即使输入有序,通过哈希函数也会进行打散得到无序状态。散列分布。

离散性指的是不同的输入离散到输出范围上的点是没规律的。均匀性就是圈的那些数都是几乎相同的。

以上四个特征进一步加工使用

这张图所要表达的意思是,对于一系列输入,通过哈希函数会得到一些输出,这些输出在s范围中,这些输出求余m之后,输出的结果会在0到m-1范围内。

下面来看一个问题,在0到2^32-1(约40亿个数)范围中找到出现次数最多的那个数,并且只会给你1G的内存

首先,我们第一个会想到的思路是使用哈希表,将重复出现的数字的次数记录在value上,这样确是可以实现,但是会使用近32G的内存。

我们采用哈希函数的性质:

如果将输出的数out每个都进行%100,那么我们会得到在0到99范围内的一些数,什么意思呢?就是我们可以把这0到99想象成100个文件,这些文件中装的一些数都是out%m之后得到的数。这个过程我们并没有使用哈希表,只是使用的哈希函数,之后我们在这100个小文件中,依次使用哈希表,那么每个小文件中肯定会得到一个次数出现最多的数,之后我们只需要在这100个文件中出现次数最多的数中找到那个最大的就可以了。

哈希表的实现

 

 

哈希表是基于哈希函数进行实现的,我们通过这张图先来简单的说一下经典的实现方法。

假设给你一个17个大小的空间,所以始末地址为0到16,第一个字符"abc"我们通过函数f()求得out,之后再%17,得到数字13,然后我们就把abc串到桶13的后面,其余字符相同的方法,如果得到的数字相同,则继续串在后面。根据哈希函数的性质,每个桶上面的链大小几乎都差不多。

下面再看看看如何扩容,假设13号桶的链的大小达到6,那么其余链也差不多大小,然后我们对其进行扩充,由17到34,然后链的话由6到3,对于链中的一些数,需要进行重新取模,然后插入到对应的桶中。

对于扩容来说,N个字符所需要消耗的扩容次数是O(logN)级别的。

每次扩容因为需要重新计算哈希值,所需要的代价是O(N)

加了N个字符串的代价是O(NlogN),那么单次的代价就是O(logN)

为什么说查找的时候效率可以是O(1)呢?因为我们可以把链定义的很长,导致O(logN)无先接近于O(1)

public class HashMapAndSortedMap {

    public static class Node {
        public int value;

        public Node(int v) {
            value = v;
        }
    }

    public static class Zuo {
        public int value;

        public Zuo(int v) {
            value = v;
        }
    }

    public static void main(String[] args) {

        HashMap<Integer, String> test = new HashMap<>();
        Integer a = 19000000;
        Integer b = 19000000;
        System.out.println(a == b);

        test.put(a, "我是3");
        System.out.println(test.containsKey(b));

        Zuo z1 = new Zuo(1);
        Zuo z2 = new Zuo(1);
        HashMap<Zuo, String> test2 = new HashMap<>();
        test2.put(z1, "我是z1");
        System.out.println(test2.containsKey(z2));

        // UnSortedMap
        HashMap<Integer, String> map = new HashMap<>();
        map.put(1000000, "我是1000000");
        map.put(2, "我是2");
        map.put(3, "我是3");
        map.put(4, "我是4");
        map.put(5, "我是5");
        map.put(6, "我是6");
        map.put(1000000, "我是1000001");

        System.out.println(map.containsKey(1));
        System.out.println(map.containsKey(10));

        System.out.println(map.get(4));
        System.out.println(map.get(10));

        map.put(4, "他是4");
        System.out.println(map.get(4));

        map.remove(4);
        System.out.println(map.get(4));

        // key
        HashSet<String> set = new HashSet<>();
        set.add("abc");
        set.contains("abc");
        set.remove("abc");

        // 哈希表,增、删、改、查,在使用时,O(1)

        System.out.println("=====================");

        Integer c = 100000;
        Integer d = 100000;
        System.out.println(c.equals(d));

        Integer e = 127; // - 128 ~ 127
        Integer f = 127;
        System.out.println(e == f);

        HashMap<Node, String> map2 = new HashMap<>();
        Node node1 = new Node(1);
        Node node2 = node1;
        map2.put(node1, "我是node1");
        map2.put(node2, "我是node1");
        System.out.println(map2.size());

        System.out.println("======================");

        // TreeMap 有序表:接口名
        // 红黑树、avl、sb树、跳表
        // O(logN)
        System.out.println("有序表测试开始");
        TreeMap<Integer, String> treeMap = new TreeMap<>();

        treeMap.put(3, "我是3");
        treeMap.put(4, "我是4");
        treeMap.put(8, "我是8");
        treeMap.put(5, "我是5");
        treeMap.put(7, "我是7");
        treeMap.put(1, "我是1");
        treeMap.put(2, "我是2");

        System.out.println(treeMap.containsKey(1));
        System.out.println(treeMap.containsKey(10));

        System.out.println(treeMap.get(4));
        System.out.println(treeMap.get(10));

        treeMap.put(4, "他是4");
        System.out.println(treeMap.get(4));

        // treeMap.remove(4);
        System.out.println(treeMap.get(4));

        System.out.println("新鲜:");

        System.out.println(treeMap.firstKey());
        System.out.println(treeMap.lastKey());
        // <= 4
        System.out.println(treeMap.floorKey(4));
        // >= 4
        System.out.println(treeMap.ceilingKey(4));
        // O(logN)

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值