上一篇博客探究了简单递归的思想,从这篇博客起,我们要探索数据结构,先从链表开始~
链表原理
链表的内存模型
单链表有两个属性:
- value,值
- next,指向下一个节点的指针(引用)
双链表附加一个属性:
- pre,指向上一个节点的指针(引用)
我们可以尝试自己写一个简单的链表:
public class ListNode<T> {
private T value;
private ListNode<T> next;
public ListNode(T value, ListNode<T> next) {
super();
this.value = value;
this.next = next;
}
public ListNode() {
super();
}
private ListNode<T> pre;
}
测试类:
@Test
public void test01() throws Exception {
ListNode<Integer> p4 = new ListNode<Integer>(4,null);
ListNode<Integer> p3 = new ListNode<Integer>(3,p4);
ListNode<Integer> p2 = new ListNode<Integer>(2,p3);
ListNode<Integer> p1 = new ListNode<Integer>(1,p2);
}
其内存模型看起来似乎是这个样子的存储结构:
但是,实际上是这个样子的:
对于这种情况:
@Test
public void test02() throws Exception {
ListNode<Integer> p1=new ListNode<Integer>(1, new ListNode<Integer>(2,
new ListNode<Integer>(3, new ListNode<Integer>(4, null))));
}
模拟链表
了解了其内存模型后,我们可以自己再尝试写一个链表,实现其如下基本功能:
创建和打印
我们可以根据数组array创建链表:
打印链表的思路:
依次打印链表,其实就是遍历
遍历的方法:
p=p.next
代码如下:
/**
* 数组转换成链表
*/
public void arrayToList(T[] array){
ListNode<T> p=head;
for(T t:array){
ListNode<T> node=new ListNode<T>(t, null);
p.next=node;
p=node;
}
}
/**
* 打印链表
*/
public void printList(){
ListNode<T> p=head.next;
while(p!=null){
System.out.print(p.value+" ");
p=p.next;
}
System.out.println();
}
插入和删除
节点的删除:
T remove(int index),删除第index个节点,并返回节点的值
- 遍历链表,找到前趋节点pre,以及节点p
- pre.next=p.next
- return p.value
查询和修改
查询:
T get(int index),返回第index个节点的值,index从0开始。
修改:void set(int index,T value),将第index个节点的值设置为value,index从0开始。
遍历!
/**
* 查询
*/
public T get(int index){
ListNode<T> p=head;
for(int i=0;i<=index;i++){
p=p.next;
}
return p.value;
}
/**
* 修改
*/
public void set(int index,T value){
ListNode<T> p=head;
for(int i=0;i<=index;i++){
p=p.next;
}
p.value=value;
}
当然,以上代码只是简单的表达思想,不够严谨。
参考LinkedList源码:
- 容器大小:size()
- 是否为空:isEmpty()
- for循环的封装
- 边界检查
- 迭代器
ArrayList、LinkedList、Stack、Queue等线性结构的源码,以后再探讨。
逆序打印链表
还是和之前一样,我通过解决问题去学习、去加深认识。
给定单链表,从尾到头打印每个节点的值,不同的值之间用空格隔开。 比如:1->2->3->4->5 输出:5 4 3 2 1 用非递归以及递归两种算法实现。 |
非递归实现:
先打印尾部、后打印头部。
自然而然联想到:先进后出的栈。
- 遍历链表,将所有的节点(值)依次压栈
- 依次弹栈、打印,直到栈为空
/**
* 逆序打印链表,非递归算法
*/
public void printInverse(){
if(head.next == null){
return;
}
Stack<T> stack = new Stack<T>();
ListNode<T> p = head.next;
while(p != null){
stack.push(p.value);
p = p.next;
}
while(!stack.isEmpty()){
System.out.print(stack.pop() + " ");
}
}
这种方法的时间、空间复杂度都为O(n)。
递归实现
思路:
void recursive(ListNode p){//对于某个节点p
if(p!=null){
recursive(p.next);//递归p的下一个节点
print(p);//打印本节点p
}
}
实现代码
/**
* 逆序打印链表,递归算法
*/
public void printInverseRecursive(){
if(head.next == null){
return;
}
recursive(head.next);
System.out.println();
}
private void recursive(ListNode<T> p){
if(p!=null){
recursive(p.next);
System.out.println(p.value + " ");
}
}
这种方法的时间、空间复杂度也都为O(n)。
链表的最大元素
对于T类型的a和b,如何比较大小?
- Comparable接口
- 自定义Comparator
/**
* 比较a和b的大小
* 如果a==b,返回0
* 如果a<b,返回负数
* 如果a>b,返回正数
*/
@SuppressWarnings("unchecked")
public Comparator<T> comp;
public int compare(T a,T b){
if(comp != null){
return compare(a, b);
}
else{
Comparable<T> c = (Comparable<T>) a;
return c.compareTo(b);
}
}
可以通过打擂台的方式以具体实现,思路如下:
T getMax(){
p=head.next;
max=p.value;//临时变量max,记录最大值
p=p.next;
while(p!=null){
//遍历链表,依次比较max与当前值的大小
if(compare(p.value, max)>0){//或者≥
max=p.value;
}
p=p.next;
}
return max;
}
实现:
/**
* 取得链表的最大值
*/
public T getMax(){
if(head.next==null){
return null;
}
ListNode<T> p=head.next;
T max=p.value;
p=p.next;
while(p!=null){
if(compare(p.value, max)>0){
max=p.value;
}
p=p.next;
}
return max;
}
这种放的时间复杂度为O(n),空间复杂度为O(1)
其他知识点
Comparable、Comparator广泛应用于其它数据结构,以及排序、查找等算法。
以后会深入探讨!
链表反转
问题描述:
给定单链表,反转这个单链表,并返回新的头节点。 例如,给定单链表:1->2->3->4->5->6 经过反转之后,变成:6->5->4->3->2->1 leetCode 206:Reverse Linked List 用递归、非递归两种算法实现。 假设链表的长度为n,要求两种算法的时间复杂度必须为O(N); 非递归算法的空间复杂度必须为O(1)。 |
非递归算法的思路
三个指针:
- pre,前趋节点
- p,当前节点
- next,下一个节点
关键的四个步骤:
- next=p.next; //把p的下一个节点存储起来
- p.next=pre; //当前节点反转
- pre=p; //继续往下遍历
- p=next; //继续往下遍历
最后再将为上图1的节点(原本的第一个节点)的next指针域,置为空。即完成了链表反转。
非递归算法的实现
/**
* 链表反转,非递归算法
*/
public ListNode reverseList(ListNode head){
if(head == null || head.next == null){
return head;
}
else{
ListNode pre = head;
ListNode p = head.next;
ListNode next = null;//缓存当前节点的指针
while(p!=null){
next = p.next;
p.next = pre;
pre = p;
p = next;
}
head.next = null;
return pre;
}
}
这种方法的时间复杂度为O(n),空间复杂度为O(1)
递归算法的思路
对于某个节点p:
- 递归处理next
- next.next=p
- 返回尾部节点
递归算法的实现
/**
* 链表反转,递归算法
*/
public ListNode reverseListRecursive(ListNode head){
if(head == null || head.next == null){
return head;
}
else
{
ListNode tail = recursive(head);
head.next = null;
return tail;
}
}
public ListNode recursive(ListNode p){
if(p.next == null){
return p;
}else{
ListNode next = p.next;
ListNode tail = recursive(next);
next.next = p;
return tail;
}
}
小结:
- 指针(引用),pre、p、next
- p=p.next
- xxx.next=yyy.next