链表的结构与数组有着很大的不同,他不像数组一样,需要一块连续的地址空间,他可以通过指针将零散的空间连接起来,更加有效的提高了空间的利用率。
单链表:
线性表的链式存储就叫做单链表,下面来看看如何进行定义:
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)
}
}