Odwracanie połączonej listy w Javie, rekurencyjnie


101

Od jakiegoś czasu pracuję nad projektem Java dla klasy. Jest to implementacja połączonej listy (nazywanej tutaj AddressList, zawierającej zwane proste węzły ListNode). Haczyk polega na tym, że wszystko musiałoby być zrobione za pomocą algorytmów rekurencyjnych. Udało mi się zrobić wszystko dobrze bez jednej metody:public AddressList reverse()

ListNode:

public class ListNode{
  public String data;
  public ListNode next;
}

W tej chwili moja reversefunkcja po prostu wywołuje funkcję pomocniczą, która przyjmuje argument, aby zezwolić na rekursję.

public AddressList reverse(){
  return new AddressList(this.reverse(this.head));
}

Dzięki mojej funkcji pomocniczej posiadającej podpis private ListNode reverse(ListNode current).

W tej chwili pracuję iteracyjnie ze stosem, ale nie tego wymaga specyfikacja. Znalazłem algorytm w C, który rekurencyjnie odwracał go i ręcznie konwertował na kod Java, i działał, ale nie rozumiałem tego.

Edycja: nieważne, w międzyczasie odkryłem to.

private AddressList reverse(ListNode current, AddressList reversedList){
  if(current == null) 
      return reversedList;
  reversedList.addToFront(current.getData());
  return this.reverse(current.getNext(), reversedList);
}

Skoro tu jestem, czy ktoś widzi jakieś problemy z tą trasą?


2
Nie, nie ma problemu z twoim rozwiązaniem. Wręcz przeciwnie, jest nawet „lepsze” niż ulubione rozwiązanie „Little Lisper”, ponieważ pozwala zachować oryginalną listę. Byłoby to szczególnie cenne w środowisku wielordzeniowym, gdzie niezmienne wartości są zdecydowanie preferowane.
Ingo

Odpowiedzi:


318

W jednej odpowiedzi znajduje się kod, który to określa, ale może się okazać, że łatwiej będzie zacząć od dołu, zadając drobne pytania i odpowiadając na nie (takie jest podejście w The Little Lisper):

  1. Jaka jest odwrotność wartości null (pusta lista)? zero.
  2. Jaka jest odwrotność listy jednoelementowej? element.
  3. Jaka jest odwrotność listy n elementów? rewers reszty listy, po której następuje pierwszy element.

public ListNode Reverse(ListNode list)
{
    if (list == null) return null; // first question

    if (list.next == null) return list; // second question

    // third question - in Lisp this is easy, but we don't have cons
    // so we grab the second element (which will be the last after we reverse it)

    ListNode secondElem = list.next;

    // bug fix - need to unlink list from the rest or you will get a cycle
    list.next = null;

    // then we reverse everything from the second element on
    ListNode reverseRest = Reverse(secondElem);

    // then we join the two lists
    secondElem.next = list;

    return reverseRest;
}

30
Wow, podoba mi się ta cała rzecz "Trzy pytania".
sdellysse,

4
Dzięki. Mała kwestia ma być podstawą nauki Lispa. Jest to również sposób na ukrycie indukcji przed newbsem, co jest zasadniczo tym, czym jest ten wzór. Polecam przeczytanie Little Lisper, jeśli naprawdę chcesz rozwiązać tego typu problem.
cokół

44
wyjątki dotyczące wyjątkowych okoliczności. Po co używać haczyka dla znanego stanu, który można przetestować za pomocą warunku „if”?
Luke Schafer

4
Uważam, że nie musisz tworzyć zmiennej: secondElem, ponieważ list.next jest nadal secondElem. Po „ListNode reverseRest = Reverse (secondElem);” możesz najpierw wykonać „list.next.next = list”, a następnie „list.next = null”. I to wszystko.
ChuanRocks

3
Czy możesz wyjaśnić, dlaczego list.next = null? Próbowałem zrozumieć cykl, ale nie zrozumiałem.
Rohit

29

Zadano mi to pytanie na wywiadzie i byłem zirytowany, że majstrowałem przy nim, ponieważ byłem trochę zdenerwowany.

Powinno to odwrócić pojedynczo połączoną listę o nazwie reverse (head, NULL); więc gdyby to była Twoja lista:

1-> 2-> 3-> 4-> 5-> null
stałoby się:
5-> 4-> 3-> 2-> 1-> null

    //Takes as parameters a node in a linked list, and p, the previous node in that list
    //returns the head of the new list
    Node reverse(Node n,Node p){   
        if(n==null) return null;
        if(n.next==null){ //if this is the end of the list, then this is the new head
            n.next=p;
            return n;
        }
        Node r=reverse(n.next,n);  //call reverse for the next node, 
                                      //using yourself as the previous node
        n.next=p;                     //Set your next node to be the previous node 
        return r;                     //Return the head of the new list
    }
    

edit: zrobiłem 6 zmian, pokazując, że jest to dla mnie trochę trudne, lol


2
Byłbym trochę urażony wymogiem „musi być rekurencyjny” w wywiadzie, szczerze mówiąc, jeśli określono Javę. W przeciwnym razie wybrałbym p = null; while (n.next! = null) {n2 = n.next; n.next = p; p = n; n = n2;} n.następna = p; powrót n ;. Stos O (N) jest przeznaczony dla ptaków.
Steve Jessop,

O tak, zerowa kontrola również na głowie, to jest Java.
Steve Jessop,

23

Przeszedłem w połowie (do zera i jeden węzeł, jak sugeruje cokół), ale straciłem ścieżkę po wykonaniu połączenia rekurencyjnego. Jednak po przeczytaniu postu przy cokole, oto co wymyśliłem:

Node reverse(Node head) {
  // if head is null or only one node, it's reverse of itself.
  if ( (head==null) || (head.next == null) ) return head;

  // reverse the sub-list leaving the head node.
  Node reverse = reverse(head.next);

  // head.next still points to the last element of reversed sub-list.
  // so move the head to end.
  head.next.next = head;

  // point last node to nil, (get rid of cycles)
  head.next = null;
  return reverse;
}

bardzo fajnie, jak robienie minusów :)
Karthikeyan D

9

Oto jeszcze jedno rozwiązanie rekurencyjne. Zawiera mniej kodu w ramach funkcji rekurencyjnej niż niektóre inne, więc może być trochę szybszy. To jest C #, ale uważam, że Java byłaby bardzo podobna.

class Node<T>
{
    Node<T> next;
    public T data;
}

class LinkedList<T>
{
    Node<T> head = null;

    public void Reverse()
    {
        if (head != null)
            head = RecursiveReverse(null, head);
    }

    private Node<T> RecursiveReverse(Node<T> prev, Node<T> curr)
    {
        Node<T> next = curr.next;
        curr.next = prev;
        return (next == null) ? curr : RecursiveReverse(curr, next);
    }
}

8

Algo będzie musiało działać na następującym modelu,

  • śledź głowę
  • Powtarzaj do końca listy linków
  • Odwróć połączenie

Struktura:

Head    
|    
1-->2-->3-->4-->N-->null

null-->1-->2-->3-->4-->N<--null

null-->1-->2-->3-->4<--N<--null

null-->1-->2-->3<--4<--N<--null

null-->1-->2<--3<--4<--N<--null

null-->1<--2<--3<--4<--N<--null

null<--1<--2<--3<--4<--N
                       |
                       Head

Kod:

public ListNode reverse(ListNode toBeNextNode, ListNode currentNode)
{               
        ListNode currentHead = currentNode; // keep track of the head

        if ((currentNode==null ||currentNode.next==null )&& toBeNextNode ==null)return currentHead; // ignore for size 0 & 1

        if (currentNode.next!=null)currentHead = reverse(currentNode, currentNode.next); // travarse till end recursively

        currentNode.next = toBeNextNode; // reverse link

        return currentHead;
}

Wynik:

head-->12345

head-->54321

7

Myślę, że jest to bardziej czystsze rozwiązanie, które przypomina LISP

// Example:
// reverse0(1->2->3, null) => 
//      reverse0(2->3, 1) => 
//          reverse0(3, 2->1) => reverse0(null, 3->2->1)
// once the first argument is null, return the second arg
// which is nothing but the reveresed list.

Link reverse0(Link f, Link n) {
    if (f != null) {
        Link t = new Link(f.data1, f.data2); 
        t.nextLink = n;                      
        f = f.nextLink;             // assuming first had n elements before, 
                                    // now it has (n-1) elements
        reverse0(f, t);
    }
    return n;
}

7

Wiem, że to stary post, ale większość odpowiedzi nie jest rekurencyjna, tzn. Wykonują pewne operacje po powrocie z wywołania rekurencyjnego, a zatem nie są najbardziej wydajne.

Oto rekurencyjna wersja ogona:

public Node reverse(Node previous, Node current) {
    if(previous == null)
        return null;
    if(previous.equals(head))
        previous.setNext(null);
    if(current == null) {    // end of list
        head = previous;
        return head;
    } else {
                    Node temp = current.getNext();
        current.setNext(previous);
        reverse(current, temp);
    }
    return null;    //should never reach here.
} 

Zadzwoń z:

Node newHead = reverse(head, head.getNext());

9
w swojej metodzie odwołujesz się do zmiennej o nazwie "head", ale nie jest ona nigdzie zadeklarowana.
maraton

prawdopodobnie jest to metoda klasy List zawierająca atrybut Node head
ChrisMcJava

4
void reverse (node1, node2) {
if (node1.next! = null)
      reverse (node1.next, node1);
   node1.next = node2;
}
wywołaj tę metodę jako reverse (start, null);

4
public Node reverseListRecursive(Node curr)
{
    if(curr == null){//Base case
        return head;
    }
    else{
        (reverseListRecursive(curr.next)).next = (curr);
    }
    return curr;
}

3
public void reverse() {
    head = reverseNodes(null, head);
}

private Node reverseNodes(Node prevNode, Node currentNode) {
    if (currentNode == null)
        return prevNode;
    Node nextNode = currentNode.next;
    currentNode.next = prevNode;
    return reverseNodes(currentNode, nextNode);
}

Myślę, że to najlepsze rozwiązanie ... proste, optymalizowalna rekurencja ogona i tylko jedno sprawdzenie zerowe.
sdanzig

2
public static ListNode recRev(ListNode curr){

    if(curr.next == null){
        return curr;
    }
    ListNode head = recRev(curr.next);
    curr.next.next = curr;
    curr.next = null;

    // propogate the head value
    return head;

}

To jest najlepsze rozwiązanie, ale nie najlepsza odpowiedź, ponieważ nie ma wyjaśnienia :). Na początku wyprowadziłem podobne rozwiązanie, ale straciłem odniesienie głowy. To rozwiązanie rozwiązuje ten problem.
OpenUserX03

2

Odwróć za pomocą rekurencyjnego algo.

public ListNode reverse(ListNode head) {
    if (head == null || head.next == null) return head;    
    ListNode rHead = reverse(head.next);
    rHead.next = head;
    head = null;
    return rHead;
}

Iteracyjnie

public ListNode reverse(ListNode head) {
    if (head == null || head.next == null) return head;    
    ListNode prev = null;
    ListNode cur = head
    ListNode next = head.next;
    while (next != null) {
        cur.next = prev;
        prev = cur;
        cur = next;
        next = next.next;
    }
    return cur;
}

Niestety, twoje rekurencyjne odwrócenie jest Złe !!
Sree Aurovindh

@SreeAurovindh - Dlaczego?
rayryeng

2

To rozwiązanie pokazuje, że żadne argumenty nie są wymagane.

/**
 * Reverse the list
 * @return reference to the new list head
 */
public LinkNode reverse() {
    if (next == null) {
        return this; // Return the old tail of the list as the new head
    }
    LinkNode oldTail = next.reverse(); // Recurse to find the old tail
    next.next = this; // The old next node now points back to this node
    next = null; // Make sure old head has no next
    return oldTail; // Return the old tail all the way back to the top
}

Oto kod pomocniczy, aby pokazać, że to działa:

public class LinkNode {
    private char name;
    private LinkNode next;

    /**
     * Return a linked list of nodes, whose names are characters from the given string
     * @param str node names
     */
    public LinkNode(String str) {
        if ((str == null) || (str.length() == 0)) {
            throw new IllegalArgumentException("LinkNode constructor arg: " + str);
        }
        name = str.charAt(0);
        if (str.length() > 1) {
            next = new LinkNode(str.substring(1));
        }
    }

    public String toString() {
        return name + ((next == null) ? "" : next.toString());
    }

    public static void main(String[] args) {
        LinkNode head = new LinkNode("abc");
        System.out.println(head);
        System.out.println(head.reverse());
    }
}

2

Oto proste podejście iteracyjne:

public static Node reverse(Node root) {
    if (root == null || root.next == null) {
        return root;
    }

    Node curr, prev, next;
    curr = root; prev = next = null;
    while (curr != null) {
        next = curr.next;
        curr.next = prev;

        prev = curr;
        curr = next;
    }
    return prev;
}

A oto podejście rekurencyjne:

public static Node reverseR(Node node) {
    if (node == null || node.next == null) {
        return node;
    }

    Node next = node.next;
    node.next = null;

    Node remaining = reverseR(next);
    next.next = node;
    return remaining;
}

1

Ponieważ Java jest zawsze przekazywana przez wartość, aby rekurencyjnie odwrócić połączoną listę w Javie, upewnij się, że na końcu rekurencji zwracana jest „nowa głowa” (węzeł główny po przywróceniu).

static ListNode reverseR(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }

    ListNode first = head;
    ListNode rest = head.next;

    // reverse the rest of the list recursively
    head = reverseR(rest);

    // fix the first node after recursion
    first.next.next = first;
    first.next = null;

    return head;
}

1

PointZeroTwo ma elegancką odpowiedź i to samo w Javie ...

public void reverseList(){
    if(head!=null){
        head = reverseListNodes(null , head);
    }
}

private Node reverseListNodes(Node parent , Node child ){
    Node next = child.next;
    child.next = parent;
    return (next==null)?child:reverseListNodes(child, next);
}

Jest to idealne rozwiązanie, ponieważ nie zawsze chcesz, aby ta metoda listy traktowała listę jako argumenty, ale odwracała się z własnymi elementami potomnymi, dziękuję
Manu

0
public class Singlelinkedlist {
  public static void main(String[] args) {
    Elem list  = new Elem();
    Reverse(list); //list is populate some  where or some how
  }

  //this  is the part you should be concerned with the function/Method has only 3 lines

  public static void Reverse(Elem e){
    if (e!=null)
      if(e.next !=null )
        Reverse(e.next);
    //System.out.println(e.data);
  }
}

class Elem {
  public Elem next;    // Link to next element in the list.
  public String data;  // Reference to the data.
}

0
public Node reverseRec(Node prev, Node curr) {
    if (curr == null) return null;  

    if (curr.next == null) {
        curr.next = prev;
        return curr;

    } else {
        Node temp = curr.next; 
        curr.next = prev;
        return reverseRec(curr, temp);
    }               
}

wywołanie przy użyciu: head = reverseRec (null, head);


0

To, co zrobili inni faceci, w innym poście jest grą treści, to, co zrobiłem, to gra z linkowaną listą, która odwraca wartość członków LinkedIn, a nie odwraca wartości członków.

Public LinkedList reverse(LinkedList List)
{
       if(List == null)
               return null;
       if(List.next() == null)
              return List;
       LinkedList temp = this.reverse( List.next() );
       return temp.setNext( List );
}

sry, zapomniałem, że potrzebujesz również metody pomocniczej, aby ustawić następny ogon, z wartością zerową
Nima Ghaedsharafi

0
package com.mypackage;
class list{

    node first;    
    node last;

    list(){
    first=null;
    last=null;
}

/*returns true if first is null*/
public boolean isEmpty(){
    return first==null;
}
/*Method for insertion*/

public void insert(int value){

    if(isEmpty()){
        first=last=new node(value);
        last.next=null;
    }
    else{
        node temp=new node(value);
        last.next=temp;
        last=temp;
        last.next=null;
    }

}
/*simple traversal from beginning*/
public void traverse(){
    node t=first;
    while(!isEmpty() && t!=null){
        t.printval();
        t= t.next;
    }
}
/*static method for creating a reversed linked list*/
public static void reverse(node n,list l1){

    if(n.next!=null)
        reverse(n.next,l1);/*will traverse to the very end*/
    l1.insert(n.value);/*every stack frame will do insertion now*/

}
/*private inner class node*/
private class node{
    int value;
    node next;
    node(int value){
        this.value=value;
    }
    void printval(){
        System.out.print(value+" ");
    }
}

 }

0

Rozwiązaniem jest:

package basic;

import custom.ds.nodes.Node;

public class RevLinkedList {

private static Node<Integer> first = null;

public static void main(String[] args) {
    Node<Integer> f = new Node<Integer>();
    Node<Integer> s = new Node<Integer>();
    Node<Integer> t = new Node<Integer>();
    Node<Integer> fo = new Node<Integer>();
    f.setNext(s);
    s.setNext(t);
    t.setNext(fo);
    fo.setNext(null);

    f.setItem(1);
    s.setItem(2);
    t.setItem(3);
    fo.setItem(4);
    Node<Integer> curr = f;
    display(curr);
    revLL(null, f);
    display(first);
}

public static void display(Node<Integer> curr) {
    while (curr.getNext() != null) {
        System.out.println(curr.getItem());
        System.out.println(curr.getNext());
        curr = curr.getNext();
    }
}

public static void revLL(Node<Integer> pn, Node<Integer> cn) {
    while (cn.getNext() != null) {
        revLL(cn, cn.getNext());
        break;
    }
    if (cn.getNext() == null) {
        first = cn;
    }
    cn.setNext(pn);
}

}


0
static void reverseList(){

if(head!=null||head.next!=null){
ListNode tail=head;//head points to tail


ListNode Second=head.next;
ListNode Third=Second.next;
tail.next=null;//tail previous head is poiniting null
Second.next=tail;
ListNode current=Third;
ListNode prev=Second;
if(Third.next!=null){



    while(current!=null){
    ListNode    next=current.next;
        current.next=prev;
        prev=current;
        current=next;
    }
    }
head=prev;//new head
}
}
class ListNode{
    public int data;
    public ListNode next;
    public int getData() {
        return data;
    }

    public ListNode(int data) {
        super();
        this.data = data;
        this.next=null;
    }

    public ListNode(int data, ListNode next) {
        super();
        this.data = data;
        this.next = next;
    }

    public void setData(int data) {
        this.data = data;
    }
    public ListNode getNext() {
        return next;
    }
    public void setNext(ListNode next) {
        this.next = next;
    }





}

0
private Node ReverseList(Node current, Node previous)
    {
        if (current == null) return null;
        Node originalNext = current.next;
        current.next = previous;
        if (originalNext == null) return current;
        return ReverseList(originalNext, current);
    }

zacznij od ReverseList (head, null)
pogłaskaj

0
//this function reverses the linked list
public Node reverseList(Node p) {
    if(head == null){
        return null;
    }
    //make the last node as head
    if(p.next == null){
        head.next = null;
        head = p;
        return p;
    }
    //traverse to the last node, then reverse the pointers by assigning the 2nd last node to last node and so on..
    return reverseList(p.next).next = p;
}

0
//Recursive solution
class SLL
{
   int data;
   SLL next;
}

SLL reverse(SLL head)
{
  //base case - 0 or 1 elements
  if(head == null || head.next == null) return head;

  SLL temp = reverse(head.next);
  head.next.next = head;
  head.next = null;
  return temp;
}

0

Zainspirowany artykułem omawiającym niezmienne implementacje rekurencyjnych struktur danych, połączyłem alternatywne rozwiązanie przy użyciu języka Swift.

Wiodące rozwiązanie w zakresie dokumentów z odpowiedziami, podkreślając następujące tematy:

  1. Co jest odwrotnością zera (pusta lista)?
    • Nie ma to znaczenia, ponieważ w Swift nie ma żadnej ochrony.
  2. Jaka jest odwrotność listy jednoelementowej?
    • Sam element
  3. Jaka jest odwrotność listy n elementów?
    • Odwrotność drugiego elementu, po którym następuje pierwszy element.

Podałem je tam, gdzie ma to zastosowanie, w rozwiązaniu poniżej.

/**
 Node is a class that stores an arbitrary value of generic type T 
 and a pointer to another Node of the same time.  This is a recursive 
 data structure representative of a member of a unidirectional linked
 list.
 */
public class Node<T> {
    public let value: T
    public let next: Node<T>?

    public init(value: T, next: Node<T>?) {
        self.value = value
        self.next = next
    }

    public func reversedList() -> Node<T> {
        if let next = self.next {
            // 3. The reverse of the second element on followed by the first element.
            return next.reversedList() + value
        } else {
            // 2. Reverse of a one element list is itself
            return self
        }
    }
}

/**
 @return Returns a newly created Node consisting of the lhs list appended with rhs value.
 */
public func +<T>(lhs: Node<T>, rhs: T) -> Node<T> {
    let tail: Node<T>?
    if let next = lhs.next {
        // The new tail is created recursively, as long as there is a next node.
        tail = next + rhs
    } else {
        // If there is not a next node, create a new tail node to append
        tail = Node<T>(value: rhs, next: nil)
    }
    // Return a newly created Node consisting of the lhs list appended with rhs value.
    return Node<T>(value: lhs.value, next: tail)
}

0

Odwracanie połączonej listy za pomocą rekursji. Chodzi o to, aby dostosować linki poprzez odwrócenie linków.

  public ListNode reverseR(ListNode p) {

       //Base condition, Once you reach the last node,return p                                           
        if (p == null || p.next == null) { 
            return p;
        }
       //Go on making the recursive call till reach the last node,now head points to the last node

        ListNode head  = reverseR(p.next);  //Head points to the last node

       //Here, p points to the last but one node(previous node),  q points to the last   node. Then next next step is to adjust the links
        ListNode q = p.next; 

       //Last node link points to the P (last but one node)
        q.next = p; 
       //Set the last but node (previous node) next to null
        p.next = null; 
        return head; //Head points to the last node
    }

1
Czy mógłbyś bardziej szczegółowo opisać swoją odpowiedź, dodając więcej opisu rozwiązania, które oferujesz?
abarisone

1
Dodałem komentarze.
Wielkie

0
public void reverseLinkedList(Node node){
    if(node==null){
        return;
    }

    reverseLinkedList(node.next);
    Node temp = node.next;
    node.next=node.prev;
    node.prev=temp;
    return;
}

-1
public void reverse(){
    if(isEmpty()){
    return;
     }
     Node<T> revHead = new Node<T>();
     this.reverse(head.next, revHead);
     this.head = revHead;
}

private Node<T> reverse(Node<T> node, Node<T> revHead){
    if(node.next == null){
       revHead.next = node;
       return node;
     }
     Node<T> reverse = this.reverse(node.next, revHead);
     reverse.next = node;
     node.next = null;
     return node;
}

-1

Oto referencja, jeśli ktoś szuka implementacji Scala:

scala> import scala.collection.mutable.LinkedList
import scala.collection.mutable.LinkedList

scala> def reverseLinkedList[A](ll: LinkedList[A]): LinkedList[A] =
         ll.foldLeft(LinkedList.empty[A])((accumulator, nextElement) => nextElement +: accumulator)
reverseLinkedList: [A](ll: scala.collection.mutable.LinkedList[A])scala.collection.mutable.LinkedList[A]

scala> reverseLinkedList(LinkedList("a", "b", "c"))
res0: scala.collection.mutable.LinkedList[java.lang.String] = LinkedList(c, b, a)

scala> reverseLinkedList(LinkedList("1", "2", "3"))
res1: scala.collection.mutable.LinkedList[java.lang.String] = LinkedList(3, 2, 1)

Byłbym bardziej niż szczęśliwy, mogąc poprawić swoją odpowiedź, gdyby osoba, która została odrzucona, wyjaśniła mi swój czyn. Tak czy inaczej, w Scali to nadal działa :)
Venkat Sudheer Reddy Aedama

Tak więc, aby zwolennik wiedział, jest to rozwiązanie rekurencyjne (w rzeczywistości rekurencyjne ogonowe).
Venkat Sudheer Reddy Aedama

Scala to nie Java, nawet jeśli obie działają na JVM.
Bill Lynch

@sharth Wow, dobrze to wiedzieć. Czy zadałeś sobie trud przeczytania pierwszej linijki mojej odpowiedzi?
Venkat Sudheer Reddy Aedama

@VenkatSudheerReddyAedama Twój głos został odrzucony, ponieważ pierwotne pytanie dotyczyło implementacji w Javie. Mimo że Scala działa w JVM, nie pomaga to w odpowiedzi na pytanie ... mimo że jest dość eleganckie. FWIW, nie przegłosowałem cię.
rayryeng
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.